Текст
                    УДК 004.43
М. Вельшенбах
Криптография на Си и C++ в действии. Учебное пособие.—
М.: Издательство Триумф, 2004 — 464 с: ил.
ISBN 5-89392-083-Х
ISBN 3-54042061-4 (нем.)
Несмотря на то, что настоящее издание содержит математическую
теорию новейших криптографических алгоритмов, книга в большей степени
рассчитана на программистов-практиков. Здесь Вы найдете описание
особенностей эффективной реализации криптографических алгоритмов на
языках Си и C++, а также большое количество хорошо документированных
исходных кодов, которые записаны на компакт-диск, прилагаемый к книге.
Купите книгу, и Вы легко сможете снабдить свои собственные
программные разработки сильной криптографической защитой.
Originally published in German.
Kryptographie in Cund C++ by Michael Welschenbach.
Copyright © Springer-Verlag Berlin Heidelberg 1998, 2001.
Springer-Verlag is a company in the BertelsmannSpringer publishing group. All rights reserved.
ISBN 5-89392-083-Х © Обложка, серия, оформление
ISBN 3-54042061-4 (нем.) ООО «Издательство ТРИУМФ», 2004


Fur Helga, Daniel und Lukas Посвящается Хелъге, Даниэлу и Лукасу
Предисловие к русскому изданию Криптография как наука имеет богатые традиции в России. Многие широко известные результаты, особенно в области математических основ современной криптографии, получены русскими учеными. Один из самых известных (по крайней мере на Западе) мастеров классической криптографии - Уильям (Вольф) Фридман (William Friedman) (1891-1969) - родился в России. Он занимался криптоанализом - искусством взлома шифров. Именно он является автором термина «криптоанализ». Искусство тайнописи веками совершенствовалось по всему миру. В России секреты тайнописи талантливых самоучек (в том числе родоначальников тайнописи - средневековых монахов и священнослужителей) были поставлены на защиту интересов государства в XVII веке. Возникла мощная дипломатическая служба, которая распространила свое влияние на всю Европу и имела обширную секретную переписку. Криптография в России продолжала развиваться и после Октябрьской революции 1917 года, в тяжелые годы Второй мировой войны, во время «холодной» войны - но почти незаметно для западной общественности. Падение Берлинской стены и объединение Германии лишь чуть приподняли завесу секретности над русскими методами шифрования, о русских методах взлома шифров по-прежнему известно очень мало. Сегодня, благодаря охватившей весь мир сети Интернет, любой желающий может получить необходимую информацию практически по любому вопросу. Однако доступность информации имеет и оборотную сторону: как никогда легко отыскать информацию, как никогда трудно ее защитить. В эпоху высокоскоростных вычислений и широкого общения роль криптографии неуклонно растет. Достоянием широкой общественности она стала после сенсационной публикации Уитфилда Диффи и Мартина Хеллмана, «открывших» в 1976 году криптографию с открытым ключом. Надеюсь, что эта книга позволит читателю познакомиться с современными криптографическими алгоритмами и понять, что и зачем он делает. Я благодарен издательству «Триумф» за интерес, проявленный к этой книге. Особая признательность ее научному редактору Павлу Семьянову за кропотливую работу. Спасибо и тебе, читатель, держащий в руках эту книгу. Кельн, Германия, март 2003 Michael Welschenbach
Предисловие ко второму изданию Когда я ломаю голову над числами, я словно зарываюсь в землю и ничего не вижу вокруг. Стоит мне лишь отвлечься, взглянуть на море, или на дерево, или на женщину - будь она даже старухой, - и мои расчеты летят ко всем чертям! Числа исчезают, обращаются в бегство, а я пытаюсь догнать их. Никое Казанзакис, Грек Зорба Во второй редакции книга была пересмотрена и значительно расширена. Добавлено несколько криптографических алгоритмов, в том числе протоколы Рабина и Эль-Гамаля; хэш-функция RIPEMD- 160 и форматы в реализации процедуры RSA приведены в соответствие с PKCS #1. Обсуждаются возможные источники ошибок, способные привести к ослаблению процедуры. Введены дополнения и пояснения по тексту, исправлены ошибки. Несколько усилена дидактическая стратегия, в результате чего некоторые программы на CD-ROM слегка отличаются от представленных в книге. Не все технические подробности одинаково важны, поскольку быстродействие программы не всегда уживается с понятным исходным текстом. Кстати о быстродействии. В приложении D приведено сравнительное время вычислений для некоторых функций библиотеки многоразрядной арифметики GMP (GNU Multi Precision Library). На их фоне процедура возведения в степень пакета FLINT/C выглядит не так уж плохо. Вдобавок в приложении F даны ссылки на некоторые арифметические и теоретико-числовые пакеты. Библиотека программ дополнена несколькими функциями, а местами существенно переработана, устранены многие ошибки и неточности. Разработаны новые тестовые функции, усилены имевшиеся ранее. Добавлен безопасный режим, теперь критические переменные в отдельных функциях уничтожаются путем затирания. Все функции на С и C++ сгруппированы по приложениям и снабжены примечаниями. Поскольку разные современные компиляторы представляют разные этапы разработки стандарта C++, модули на C++ пакета FLINT/C построены так, что можно использовать как традиционные в C++ заголовки файлов вида xxxxx.h, так и новые ANSI-заголовки. Из тех же соображений выполняется проверка при использовании оператора new(): не возвращается ли пустой указатель. При такой обработке ошибок, с одной стороны, не нужны исключения по стандарту ANSI, а с другой стороны, она поддерживается современными компиляторами, тогда как метод, согласованный со стандартом (при котором new() сообщает об ошибке через throw()), годится не всегда.
8 Криптография на Си и C++ в действии Хотя эта книга и посвящена в основном криптографии с открытым ключом, недавнее утверждение алгоритма Rijndael Американским национальным институтом стандартов и технологий (NIST) в качестве нового стандарта шифрования (Advanced Encryption Standard - AES) вдохновило меня на написание последней главы (глава 19), содержащей подробное описание алгоритма. Я очень обязан Gary Cornell из Apress, который обратил мое внимание на Rijndael и благодаря которому книга была дополнена столь ценным материалом. Хочу поблагодарить также Vincent Rijmen, Antoon Bosselaers, Paulo Barreto и Brian Gladman, позволивших включить в компакт-диск, прилагаемый к этой книге, программную реализацию алгоритма Rijndael. Спасибо всем читателям первого издания, особенно тем, кто сообщал об ошибках, делал ценные замечания, предлагал пути улучшения книги. Все такие контакты были для меня чрезвычайно полезны. По-прежнему всю ответственность как за оставшиеся, так и за возникшие ошибки в тексте книги или в программах несет исключительно автор. Мои сердечные признания Hermann Engesser, Dorothea Glaunsinger и Ulrike Strieker из издательства Springer-Verlag за безграничное доверие и сотрудничество. Я глубоко благодарен моему американскому переводчику Дэвиду Крамеру (David Kramer), компетентно и самоотверженно потрудившемуся и давшему множество ценных советов, которые были использованы и в немецком издании этой книги. Предупреждение Прежде чем где-либо использовать программы, содержащиеся в этой книге, внимательно прочитайте руководство пользователя и техническое описание соответствующего программного обеспечения и компьютера. Ни автор, ни издательство не несут ответственности за ущерб, вызванный некорректным выполнением инструкций и программ, содержащихся в этой книге, или ошибками в тексте или в программах, которые, несмотря на тщательную проверку, все же могут остаться. Программы, содержащиеся на CD-ROM, защищены авторскими правами и не могут быть воспроизведены без разрешения издательства.
Предисловие к первому изданию Математика - царица наук, теория чисел - царица математики. Иногда она снисходит до того, чтобы помочь астрономии и другим есте- ; >*;. "m'ri wt^*/; « ственным наукам, но при любых обстоятельст- .;.,лю v>, . 4v» - вах она - первая. Карл Фридрих Гаусс Зачем нужна книга по криптографии, посвященная в основном арифметике целых чисел и ее реализации в виде компьютерных программ? Насколько это важно по сравнению с теми большими задачами, которые решает программист? Если ограничиться лишь теми числами, которые могут быть описаны стандартными числовыми типами какого-либо языка программирования, арифметика будет делом довольно легким: обычные арифметические операции задаются в программах обычными символами +, -,/,*. .'«ION MC^NW!?/ Но как только нам нужны результаты, длина которых намного больше 16 или 32 битов, все становится гораздо интересней. Для таких чисел даже простые арифметические операции уже не годятся, и приходится потрудиться над разрешением таких проблем, кото- х* *мшчы'' t...^ рые раньше и проблемами-то не казались. С этим сталкивается лю- шф**о к* ;.*.*<:;♦< бой, будь то профессионал или любитель, кто занимается теорией чисел: пытаясь применить школьные алгоритмы арифметики, мы вдруг оказываемся втянутыми в невероятно запутанный процесс. hfb?q ; л*л;; Читатель, который собирается разрабатывать программы в этой об- ♦ » ; •• >»г/ I, ? ласти и не желает изобретать колесо, найдет в этой книге целый ряд функций, оперирующих с большими числами, на языках С и C++. Речь идет отнюдь не об «игрушечных» примерах, поясняющих «как это работает в принципе», но о готовом пакете функций и методов, удовлетворяющих профессиональным требованиям в части корректности, быстродействия и серьезной теоретической базы. и, frj , ' Цель этой книги - связать теорию и практику, перекинуть мост че- >,? wn.vr рез пропасть, разделяющую теоретическую литературу и практиче- v й )',</ ские задачи программирования. Последовательно, шаг за шагом мы будем познавать фундаментальные принципы арифметики больших (гтл^чк. 1 •'*.;''/| натуральных чисел, арифметики конечных колец и полей, сложные пщ <н, '.tv: i функции элементарной теории чисел, что позволит пролить свет на многочисленные и разнообразные возможности применения этих принципов в современной криптографии. Сведения из математики приводятся здесь в объеме, необходимом для понимания представленных программ; более глубокие знания можно почерпнуть из обширного списка литературы. Все разработанные нами функции постепенно объединяются и многократно тестируются, так что в итоге мы получаем полезный объемлющий программный интерфейс. Ъ:ГЛ л'Г'МГ* /.мнил»
1 о Криптография на Си и C++ в действии Мы начинаем с представления больших чисел и с изучения основных вычислительных операций, создавая для сложения, вычитания, умножения и деления больших чисел мощные базовые функции. Исходя из этого, мы поясняем модульную арифметику в классах вычетов и реализуем соответствующие операции в виде библиотечных функций. Отдельная глава посвящена трудоемкому процессу возведения в степень, 1де разрабатываются и программируются различные специальные алгоритмы модульной арифметики. После тщательной подготовки, включающей в себя также ввод и вывод больших чисел и их преобразование в различных системах счисления, мы рассматриваем элементарные теоретико-числовые алгоритмы, используя для этого базовые арифметические операции, а затем разрабатываем программы, начиная с вычисления наибольшего общего делителя больших чисел. Следом идут такие задачи, как вычисление символов Лежандра и Якоби, обращение и возведение в квадрат в конечных кольцах. Мы знакомимся также с китайской теоремой об остатках и ее приложениями. Попутно мы несколько подробнее останавливаемся на принципах распознавания больших простых чисел и программируем мощный тест простоты. Следующая глава посвящена генерации больших случайных чисел, разработке и проверке статистических свойств криптографически стойкого генератора случайных битов. Завершается первая часть тестированием арифметических и других функций. Для этого, исходя из математических правил арифметики, мы разрабатываем специальные методы проверки, а также обсуждаем реализацию эффективных внешних средств. Во второй части мы шаг за шагом строим класс LINT (Large INTe- gers - большие целые числа) на языке C++. Для этого функциям на С из первой части мы придаем синтаксис и семантику объектно- ориентированного языка C++. Значительное внимание уделено форматированному вводу и выводу LINT-объектов с гибкими потоковыми функциями и манипуляторами, а также обработке ошибок. Элегантность, с которой алгоритмы формулируются на C++, особенно поражает, когда начинают стираться границы между стандартными типами и большими числами как LINT-объектами. Отсюда - синтаксическое сходство, ясность и прозрачность реализованных алгоритмов. И наконец, мы иллюстрируем практичность разработанных методов на примере знаменитой криптосистемы RSA: для шифрования с открытым ключом и цифровой подписи. Попутно мы приводим теоретическое обоснование процедуры RSA как наиболее известного представителя асимметричных криптосистем. Отдельно разрабатывается расширяемое ядро, позволяющее применять этот ультрасо-
Предисловие к первому изданию 11 временный криптографический процесс, исходя из принципов объектно-ориентированного языка программирования C++. В завершение мы кратко остановимся на дальнейших путях расширения нашей библиотеки программ. Это, например, четыре функции умножения и деления на языке Ассемблера 80x86, которые позволяют повысить производительность наших программ. В приложении D приведена таблица типичного времени вычислений с использованием Ассемблера и без него. Я искренне призываю всех читателей этой книги присоединиться ко мне на этом пути или хотя бы, в зависимости от наклонностей, изучить отдельные параграфы или главы и испытать представленные там функции. Автор надеется, что читатели не обидятся за слово «мы», под которым он подразумевал и их, и себя. Тем самым он призывает читателей стать активными участниками увлекательного путешествия по бескрайним просторам математики и программирования, постичь эту науку и извлечь из этой книги максимальную пользу. Что же касается программ - любой читатель сможет удовлетворить свои амбиции, совершенствуя те или иные функции для новых приложений. Я благодарен издательству Springer-Verlag и, в частности, Hermann Engesser, Dorotea Glaunsinger и Ulrike Striker за интерес, проявленный к этой книге, и за активное сотрудничество. Рукопись читали Jorn Garbers, Josef von Helden, Brigitte Nebelung, Johannes Ueberberg и Helga Welschenbach. Всем им - моя сердечная признательность за высказанные критические замечания и предложения и, кроме того, за внимание и терпение. Если, несмотря на все наши усилия, ошибки в тексте и программах все еще остались, в них повинен только автор. Большое спасибо моим друзьям и коллегам Robert Hammelrath, Franz-Peter Heider, Detlef Kraus и Brigitte Nebelung, сумевшим постичь связь между математикой и программированием, за долгие годы сотрудничества, которые так много для меня значили.
Часть I # Арифметика и теория чисел на С Легко понять, насколько важна арифметика и вообще искусство математики: нет ничего, что не было бы связано с числами, не имело бы размеров; никакое искусство не может существовать без измерений и пропорций. Адам Райе, Вычисления, 1574 Типографские правила обработки литер сродни арифметическим правилам обработки чисел. Д.Р. Хофштадтер, Гедель, Эшер, Бах: эта бесконечная гирлянда Человеческий мозг никогда больше не отяготят никакие вычисления! Талантливые люди снова смогут думать, а не изводить бумагу на числа. Стэн Надольный, Открытие медлительности
ГЛАВА 1. Введение Целые числа сотворил Бог. Все остальное - дело рук человеческих. , , Леопольд Кронекер ,,ь- Если вы посмотрите на нуль, то не увидите ни- ,1v , • чего; но взгляните сквозь него - и вы увидите мир. л \ . ' Роберт Каплан, Естественная история Нуля (.;П;, В наши дни занятие криптографией волей-неволей влечет за собой углубленное изучение теории чисел, а именно, изучение нату- ,, - . " :< ральных чисел, которые представляют одну из занимательнейших областей математики. Однако мы не станем уподобляться глубоко- ,, и , водным ныряльщикам, чтобы добыть со дна математического океа- .<.,,}, %ir. на затонувшие сокровища, - для криптографических приложений ., ,% ,Г1 это не требуется. Наша цель намного скромнее. С другой стороны, внедрение теории чисел в криптографию беспредельно глубоко, и it .. / ; этому способствовали многие выдающиеся математики. 'г •{ Теория чисел уходит корнями в античность. Уже в VI веке до н. э. 1 <"Ч; пифагорейцы (греческий математик и философ Пифагор и его 1 школа) серьёзно занимались целыми числами и достигли значи- ' • ■ тельных математических результатов, например, таких как знаме- 1 нитая теорема Пифагора, известная сейчас каждому школьнику. *-г ■( » s С религиозным рвением они утверждали, что все числа соотносятся • * "*• с натуральными числами, и оказались перед серьёзной дилеммой, '•• "пл когда открыли существование иррациональных чисел, таких как -*ч .. • ,; , ? . V2 , которые нельзя представить в виде частного двух целых. Это -. ь ,. *< , . • открытие до такой степени не укладывалось в представление пифа- .*.»•. \ ,~ горейцев о мире, что они избрали достойную сожаления тактику '**•*•- г: , поведения, часто повторяющуюся в истории человечества, - поста- *■'.* ' : ,. рались утаить знание об иррациональных числах. *' i ' От греческих математиков Евклида (III век до н.э.) и Эратосфена ..r„/^',W ,\ (276-195 гг. до н.э.) до нас дошли два древнейших алгоритма тео- *. " " рии чисел. Они тесно связаны с самыми современными алгоритма- g, ( ми шифрования, которые мы используем повседневно для надежной связи через Интернет. Алгоритм Евклида и «решето» Эратосфена полностью отвечают целям нашей работы, и мы рассмотрим их теорию и применение в пп. 10.1 и 10.5 данной книги.
16 Криптография на Си и C++ в действии К числу главных основоположников современной теории чисел относятся Пьер Ферма (1601-1665), Леонард Эйлер (1707-1783), Ад- риен Мари Лежандр (1752-1833), Карл Фридрих Гаусс (1777-1855) и Эрнст Эдуард Куммер (1810-1893). Их работы образуют фундамент для современного развития этой области математики и, в частности, таких интересных прикладных областей, как криптография, с её несимметричными процедурами шифрования и формирования цифровой подписи (см. главу 16). Можно было бы упомянуть еще многих, внесших важный вклад в эту область, кто и по сей день участвует в зачастую драматических перипетиях эволюции теории чисел. Тем, кому интересны захватывающие описания из истории теории чисел, я настоятельно рекомендую книгу Саймона Сайна «Последняя теорема Ферма» (Simon Singh, Ferma's Last Theorem). С самого детства мы воспринимаем счет как нечто само собой разумеющееся и легко принимаем на веру такие факты, как «два плюс два равняется четырем». Поэтому нам покажутся неожиданными абстрактные мысленные построения, к которым придется обратиться для теоретического обоснования таких утверждений. Например, теория множеств позволяет вывести существование и арифметику натуральных чисел из почти ничего. Это «почти ничто» является пустым множеством 0 := {}, то есть множеством, которое не содержит никаких элементов. Если допустить, что пустое множество соответствует числу 0, тогда можно построить следующие дополнительные множества. Последующий элемент за 0 - 0+ соответствует множеству 0+:={0} = {0}, которое содержит единственный элемент, а именно, пустое множество. Присвоим последующему элементу 0 наименование 1. Для этого множества мы тоже можем определить последующий элемент, а именно 1+:={0, {0}}. Последующему элементу 1, содержащему 0 и 1, присвоим наименование 2. Построенные таким образом множества, которым мы так опрометчиво присвоили наименования 0, 1 и 2, мы отождествляем - что неудивительно - с хорошо известными числами 0, 1 и 2. Такой принцип построения, когда каждому числу х ставится в соответствие последующий элемент х+ :=хи {х} посредством присоединения х к предыдущему множеству, можно продолжить далее. Каждое полученное таким образом число, за исключением нуля, само является множеством, чьи элементы представляют его предшественников. Только нуль не имеет предшественников. Чтобы гарантировать продолжение этого процесса до бесконечности, в теории множеств сформулировано особое правило, называемое аксиомой бесконечности: существует множество, которое содержит 0, а также последующий элемент для каждого своего элемента.
ГЛАВА 1. Введение 17 Принимая без доказательства существование по крайней мере одного так называемого последующего множества, которое, начиная с О, содержит все последующие элементы, теория множеств выводит существование минимального последующего множества IN, которое само является подмножеством каждого последующего множества. Это минимальное и однозначно определенное последующее множество IN называется множеством натуральных чисел, в которое мы специально включаем 0 в качестве элемента.1 Натуральные числа можно охарактеризовать с помощью аксиом Джузеппе Пеано (1858-1932), которые совпадают с нашим интуитивным представлением о целых числах: (I) Последующие элементы двух неравных натуральных чисел неравны: Из п Ф т следует, что п+ Ф т+ для всех л, т е ОМ. (II) Каждое натуральное число, за исключением нуля, имеет предшественника: 0SI+= (N \{0}. (III) Принцип полной индукции: Если S с IN, 0 е S, и из п е S всегда следует, что п+ е S , то S = IN. Принцип полной индукции позволяет вывести интересующие нас арифметические операции над целыми числами. Фундаментальные операции сложения и умножения можно вывести рекурсивно следующим образом. Начнем со сложения: Для каждого натурального числа п е DSI существует функция sn: IN —> IN, такая, что (а) sn(0) = n (б) sn(x+) = (sn(x))+ для всех натуральных чисел л: е DM. Значение функции sn(x) называется суммой п + х чисел пнх. Однако существование таких функций sn для всех натуральных чисел п е ОМ требуется доказать, так как бесконечно большое количество натуральных чисел не оправдывает априори такого допущения. Для доказательства следует вернуться к принципу полной индукции в соответствии с вышеупомянутой третьей аксиомой Пеано (см. [Halm], главы 11-13). Операция умножения выводится аналогичным образом: При этом не имеет значения тот факт, что в соответствии со стандартом DIN 5473 нуль не относится к натуральным числам. В информатике, как правило, принято начинать счет с 0, а не с 1. Это указывает на важную роль нуля как нейтрального элемента при сложении (аддитивное тождество).
18 Криптография на Си и C++ в действии Для каждого натурального числа п е DM существует функция рп: DM —> ОМ, такая, что (а) Рп(0) = 0 (б) рп(х+) =рп(х) + п для всех натуральных чисел х е ИМ. Значение функции рп(х) называется произведением п • х чисел п и х. Как и следовало ожидать, умножение определено в терминах сложения. Для арифметических операций, определенных таким образом, можно доказать, применяя повторно полную индукцию от х в соответствии с Аксиомой III, такие известные арифметические законы, как ассоциативность, коммутативность и дистрибутивность (см. [Halm], глава 13). Хотя обычно мы используем эти законы без всяких церемоний, оговоримся, что будем очень часто обращаться к их помощи при тестировании FLINT-функций (см. главы 12 и 17). Подобным же образом получаем определение возведения в степень. Мы приведем его здесь ввиду важности этой операции в дальнейшем. Для каждого натурального числа п е IN существует функция еп\ ЯМ —> DM, такая, что (а) еп(°)=1 (б) еп(х+) = еп(х) • п для всех натуральных чисел х е DM. Значение функции еп(х) называется степенью х пх числа п. Используя полную индукцию, можно доказать правила возведения в степень пх- пу = пх+\пх- тх = (п- ш)\ (пх)у = пху, к которым мы вернемся в главе 6. В дополнение к вычислительным операциям, на множестве DM натуральных чисел определено отношение порядка «<», позволяющее сравнивать два элемента п, т е DM. Несмотря на важность этого факта в теории множеств, здесь мы отметим лишь, что отношение порядка обладает точно теми же свойствами, которые мы знаем и используем в нашей повседневной жизни. Начав с установления пустого множества в качестве единственного фундаментального блока для построения натуральных чисел, приступим теперь к изучению материалов, с которыми нам предстоит работать в дальнейшем. Теория чисел большей частью рассматривает натуральные и целые числа как данные и с ходу приступает
ГЛАВА 1. Введение 19 к изучению их свойств. Тем не менее, нам интересно хотя бы раз взглянуть на процесс «математического клеточного деления» - процесс, выдающий в результате не только натуральные числа, но также арифметические операции и правила, с которыми мы будем очень тесно взаимодействовать. 1.1. О программном обеспечении Программное обеспечение, описанное в этой книге, в целом представляет собой пакет, так называемую библиотеку функций, к которым часто обращаются. Название этой библиотеки - FLINT/C - является аббревиатурой для «Functions for Large Integers in Number Theory and Cryptography» (функции для больших целых в теории чисел и криптографии). FLINT/C содержит, среди всего прочего, следующие модули, которые можно найти в виде кода (текста программы) на сопроводительном CD-ROM. Таблица 1.1. Арифметика и теория чисел на С в лиректории flint/src •оЫ flint.h flint.c kmul.h, ripemd с h,c Заголовочный файл для использования функций из flint.c Функции арифметики и теории чисел на языке С Функции для умножения и возведения в квадрат по методу Караиубы Реализация хэш-функции RIPEMD-160 Таблица 1.2. Арифметика и теория чисел на C++ в лиректории flint/src flintpp.h flintpp.cpp Заголовочный файл для использования функций из flintpp.cpp Функции арифметики и теории чисел на языке C++. Этот модуль использует функции из flint.c Таблица 1.3. Арифметический молуль на Ассемблере 80x86 (см. главу 18) ' в лиректории flint/src/asm mult.asm umul.asm sqr.asm div.asm tv' Умножение, заменяет С-функиию mult() из flint.c Умножение, заменяет С-функиию umul() Возведение в квадрат, заменяет С-функиию sqr() Деление, заменяет С-функиию divJQ
20 Криптография на Си и C++ в действии Таблица 1.4. Арифметические библиотеки на Ассемблере 80x86 (см. главу 18) в лиректории flint/lib Таблииа 1.5. Тесты (см. п. 12.2 и главу 17) в лиректории flint/test Таблииа 1.6. Реализация RSA (см. главу 16) в лиректории flint/rsa flinta.lib flintavc.lib flinta.a libflint.a Библиотека ассемблерных функций в формате OMF Библиотека ассемблерных функций в формате COFF Архив ассемблерных функций для emx/gcc пол OS/2 " ™*'* Архив ассемблерных функций для использования под LINUX testxxx.c[pp] Тестовые программы на С и C++ rsakey.h rsakey.cpp rsademo.cpp Заголовочный файл для классов RSA Реализация классов RSA RSAkey и RSApub Пример применения классов RSA и их функций Список отдельных составляющих программного пакета FLINT/C можно найти в файле readme.doc на CD-ROM. Программы пакета были протестированы с помощью указанных средств разработки на следующих платформах: • GNU gcc под Linux, SunOS 4.1 и Sun Solaris • GNU/EMX gcc под OS/2 Warp, DOS и Windows (9x, NT) • lcc-win32 под Windows (9x, NT, 2000) • Cygnus cygwin B20 под Windows (9x, NT, 2000) • IBM VisualAge под OS/2 Warp и Windows (9x, NT, 2000) • Microsoft С под DOS, OS/2 Warp и Windows (9x, NT) • Microsoft Visual C/C++ под Windows (9x, NT, 2000) • Watcom C/C++ под DOS, OS/2 Warp и Windows (3.1, 9x, NT). Ассемблерные программы можно транслировать с помощью Microsoft MASMZ или Watcom WASM. Они содержатся в скомпилированном виде на CD-ROM в виде библиотек в форматах OMF (формат объектного модуля) и COFF (общий формат объектного файла), 2 Вызов: ml /Cx /с /Gd <имя файла>
ГЛАВАМ- Введение 21 ■'•JT'i. соответственно, а также в виде LINUX-архива. Эти программы можно использовать вместо соответствующих С-функций, когда при трансляции С-программ определен макрос FLINT_ASM и подключены ассемблерные объектные модули из библиотек (архивов). Типичный вызов компилятора, в данном случае для GNU gcc, выглядит примерно так (пути к исходным директориям скрыты): дсс -02 -DFLINT_ASM -о rsademo rsademo.cpp rsakey.cpp flintpp.cpp flint.c ripemd.c -Iflint -lstdc++ Заголовочные файлы C++, соответствующие стандарту ANSI, используются, когда при компиляции определен макрос FLINTPP_ANSI; в противном случае используются традиционные заголовочные файлы xxxxx.h. В зависимости от компьютерной платформы возможны отклонения, касающиеся опций компилятора. Но для достижения максимальной производительности всегда следует включать опции оптимизации \:..) по скорости. Из-за требований к стеку для многих операционных ,!,' сред и приложений он должен быть правильно настроен3. Что каса- i ется необходимого размера стека для конкретных приложений, следует отметить замечание о функциях возведения в степень в главе 6 и обзор на странице 140. Стековые требования можно сделать менее жесткими, используя функцию возведения в степень с динамическим распределением стека, а также посредством реализа- 'х ' ции динамических регистров (см. главу 9). "У* г: > „;, «, Функции и константы языка С описываются с использованием n.q nm - ' макросов scjru: FLINT_API Спецификатор для С-функций FLINT_API_A Спецификатор для ассемблерных функций s ?. FLINT_API_DATA Спецификатор для констант как например, **. j =* ^^ii't extern int_FLINT_API addj (CLINT, CLINT, CLINT); , t iK extern USHORT _FLINT_API_DATA smallprimes[]; \)J или, соответственно, при использовании ассемблерных функций extern int _FLINT_API_A divj (CLINT, CLINT, CLINT, CLINT); В современных компьютерах с виртуальной памятью, за исключением системы DOS, этот момент обычно не вызывает затруднений, особенно в случае операционных систем UNIX или LINUX
22 Криптография на Си и C++ в действии Эти макросы, в общем случае, определены как пустые комментарии /* */. С их помощью, используя соответствующие описания, можно создавать специфические для данного компилятора и компоновщика инструкции. Если используются ассемблерные модули, а компилятор GNU gcc не используется, то макрос FLINT_API_A определяется как cdecl, и некоторые компиляторы воспринимают, что будет вызываться ассемблерная функции с соответствующими С-именем и способом передачи параметров. Для модулей, которые импортируют функции и константы библиотеки FLINT/C из динамически подключаемой библиотеки (dynamic link library - DLL) под Microsoft Visual C/C++, при трансляции требуется определять макросы -D_FLINT_API=_cdecl и -D_FLINT_API_DATA=_declspec(dllJmport). В заголовочном файле flint.h это уже учтено, и в этом случае для компиляции достаточно определить макрос FLINTJJSEDLL Для других сред разработки следует представить аналогичные описания. Небольшой объём работы, связанной с инициализацией DLL, использующих FLINT/C, берет на себя функция FLINTInitJO, которая задает начальные значения для генератора случайных чисел4 и генерирует набор динамических регистров (см. главу 9). Дополнительная функция FLINTExitJO освобождает динамические регистры. Вполне разумно, что инициализация производится не в каждом отдельном процессе, использующем DLL, а выполняется один раз при старте DLL. Как правило, следует использовать функцию с определенной разработчиком сигнатурой и способом передачи параметров, которая выполняется автоматически, когда исполняющая система (run-time) загружает DLL. Эта функция может взять на себя инициализацию FLINT/C и использовать две упомянутые выше функции. Все это следует учитывать при разработке DLL. Пришлось потрудиться, чтобы сделать программное обеспечение применимым для приложений, критичных к безопасности. Для этого в режиме безопасности локальные переменные функций, в частности CLINT- и LINT-объекты, удаляются после использования посредством обнуления (записи на их место нулей). Для С-функций это достигается с помощью макроса PURGEVARS_L() и соответствующей функции purgevars_l(). Для С++-функций подобным же образом устроен деструктор ~LINT(). Ассемблерные функции зати- Г Эти начальные значения получаются из 32-разрядных чисел, задаваемых системными часами. Для приложений, где безопасность критична, в качестве начальных значений советуем использовать подходящие случайные значения из достаточно большого интервала.
ГЛАВА 1. Введение 23 рают свою рабочую память. За удаление переменных, которые были переданы функциям в качестве аргументов, отвечают вызывающие функции. Если из-за определенных дополнительных затрат времени все же нужно пропустить удаление переменных, следует определить макрос FLINTJJNSECURE. Во время выполнения функция char* verstr_l() выдает информацию о режимах, установленных во время компиляции. При этом дополнительно к метке версии Х.х в строке символов выводятся литеры «а» для ассемблерной поддержки и «s» для режима безопасности, если эти режимы были включены. 1.2. Законные условия использования программного обеспечения Данное программное обеспечение предназначено исключительно для личного использования. В этих целях программное обеспечение можно использовать, изменять и передавать при следующих условиях: 1. Запрещается изменять или удалять замечание об авторских правах. 2. Все изменения должны быть снабжены комментариями. Любое другое использование, в частности, использование программного обеспечения в коммерческих целях, требует наличия письменного разрешения от издателя или автора. Программное обеспечение было разработано и протестировано всеми доступными средствами. Так как никогда нельзя полностью исключить наличие ошибок, то ни автор, ни издатель не несут ответственности за прямой или косвенный ущерб, который может быть вызван применением или неприменением программного обеспечения, вне зависимости от того, с какими целями оно использовалось. 1.3. Как связаться с автором Автор будет рад получить сведения об ошибках и любую другую полезную критику или замечания. Пожалуйста, пишите по адресу kryptographie@welschenbach.de5. Обо всех ошибках в русском издании, пожалуйста, сообщайте редактору по адресу: oavel@semianov.com с пометкой «Криптография на С и C++» - Прим. ред.
ЛАВА 2. Числовые форматы: представление больших чисел в языке С хш ит IV'ч iO VJ>. ;П * :, f ' ■ ' 'Ю- 'Ч"| )Я*>< ■ 1 Г*' '6\ ж Итак, я придумал собственную систему записи больших чисел и, пользуясь случаем, разъясню ее в этой главе. Айзек Азимов, Дополнительное измерение Процесс, который привел эту форму на более высокую ступень организации, можно было изобразить и по-другому. № Дж. Вебер, •;^ Форма, движение, цвет Приступая к созданию библиотеки функций для работы с большими числами, в первую очередь следует определить, как представлять ,v > эти числа в оперативной памяти компьютера. Эта задача требует г тщательно продуманного решения, поскольку впоследствии пересмотреть его будет трудно. Конечно, всегда можно внести изменения во внутреннюю структуру библиотеки программ, но пользовательский интерфейс должен оставаться настолько стабильным, насколько это возможно в смысле «совместимости снизу вверх». Необходимо определить порядок размера чисел, которые придется обрабатывать, и тип данных, который будет использоваться для кодирования этих численных величин. В основе всех программ библиотеки FLINT/C лежит обработка многоразрядных натуральных чисел, длина которых (несколько сотен разрядов) намного превосходит допустимый размер стандартных типов данных. Таким образом, нам требуется логическое упорядочение ячеек компьютерной памяти, с помощью которого можно выражать большие числа и производить над ними различные операции. В связи с этим можно было бы вообразить себе некие структуры, создающие пространство именно такого размера, который потребуется для представления этих чисел. Такие экономящие оперативную память служебные средства поддерживались бы программой управления динамической памятью для больших чисел, которая по мере необходимости выделяет или освобождает память при выполнении арифметических операций. Конечно, такие средства можно реализовать (см. например, [Skal]), однако управление памятью увеличивает время вычислений, поэтому в пакете с-ты »■
26 Криптография на Си и C++ в действии FLINT/C для представления целых чисел предпочтение отдается более простому их определению со статической длиной. Большие натуральные числа можно представлять в виде векторов, г*М элементы которых являются каким-либо стандартным типом данных. В целях эффективности предпочтительнее использовать беззнаковый тип данных, который позволяет без потерь хранить результаты арифметических операций в этом типе, как например unsigned long (описанный в flint.h как ULONG), который является наибольшим арифметическим стандартным типом данных в С (см. [Harb], п. 5.1.1). Обычно ULONG-переменные можно целиком представить регистровым словом процессора. Наша цель заключается в том, чтобы свести при компиляции операции над большими числами по возможности непосредственно к регистровой арифметике процессора, ибо операции с регистрами компьютер выполняет, можно сказать, «в уме». Поэтому в пакете FLINT/C большие целые числа представляются типом unsigned short int (в дальнейшем USHORT). Мы полагаем, что тип USHORT занимает 16 бит и что тип ULONG полностью воспринимает результаты арифметических операций с типами USHORT, то есть соотношения размеров между этими типами можно выразить неформально как USHORT x USHORT < ULONG. Выполняются ли эти положения для отдельного компилятора, можно узнать из заголовочного файла ISO limits.h (см. [Harb], пп. 2.7.1. и 5.1). Например, в файле limits.h для компилятора GNU C/C++ (см. [Stlm]) появляются следующие строки: #define UCHAR_MAX OxffU #define USHRT_MAX OxffffU #define UINT_MAX OxffffffffU #define ULONG_MAX OxffffffffUL Отметим, что фактически есть только три размера, которые различаются числом двоичных разрядов. Тип USHRT (в нашем представлении соответственно USHORT) представляется 16-битовым регистром; тип ULONG - 32-битовыми регистрами. Значение ULONG_MAX определяет величину наибольших беззнаковых целых чисел, представимых скалярными типами (см. [Harb], стр. ПО)1. Наибольшая величина произведения двух чисел типа USHRT равна Oxffff * Oxffff = OxfffeOOOl и, следовательно, представима типом ULONG, где младшие 16 бит, в нашем примере 0x0001, можно выделить операцией приведения типов к типу USHRT. Реализация Без учета используемых на практике нестандартных типов, таких как unsigned long long в GNU C/C++ и в некоторых других компиляторах С.
ГЛАВА 2. Числовые форматы 27 ) ЛОЗ* > .0 . 90ГЛ' "Ч ТУГ'. основных арифметических функций пакета FLINT/C основывается на обсуждавшемся выше соотношении размеров между типами USHORT и ULONG. Аналогичным образом, используя типы данных длиной 32 бита и 64 бита вместо USHORT и ULONG, можно сократить время вычислений для операций умножения, деления и возведения в степень почти на 25 процентов. Такие возможности можно реализовать с помощью функций, написанных на Ассемблере, с прямым доступом к 64-битовым результатам машинных команд умножения и деления, или с помощью процессора с 64-битовыми регистрами, который также позволяет приложениям на языке С без потерь хранить полученные результаты в типе ULONG. Пакет FLINT/C содержит несколько примеров того, как можно ускорить получение результатов, используя арифметические ассемблерные функции (см. главу 18). Следующий вопрос - как упорядочить USHORT-разряды внутри вектора. Можно рассмотреть две возможности: слева направо, с убыванием значения разрядов от меньшего адреса ячейки памяти к большему, или наоборот, с возрастанием значения разрядов от меньшего адреса к большему. Второй вариант, противоположный нашей обычной системе обозначений, удобен тем, что размер чисел с постоянным адресом можно изменять просто добавляя разряды и не перемещая ничего в памяти. Таким образом, выбор ясен: разряды нашего числового представления возрастают с возрастанием адресов ячейки памяти или индексов вектора. В дальнейшем число разрядов будет рассматриваться как часть этого представления и храниться в первом элементе вектора. Таким образом, представление чисел большой длины в памяти выглядит так: п = (1щп1...п1)в, 0</<CLINTMAXDIGIT, 0 < щ < В (/ = 1, ..., /), где В обозначает основание числового представления; для пакета FLINT/C В := 216 = 65536. Это значение В будет постоянно участвовать в дальнейшем изложении. Константа CLINTMAXDIGIT определяет максимальное число разрядов в CLINT-объекте. Нуль представляется длиной / = 0. Значение п числа, представляемого FLINT/C-переменной п_1, вычисляется как п_1[0] £nj[i]5";, если nj[0]>0, О, в противном случае. Если п > 0, то младший разряд числа п по основанию В задается п_1[1], а старший разряд - n_l[n_l[0]]. Число разрядов n_l[0] в дальнейшем будет считываться макросом DIGITS_L (n_l) и помещаться в / макросом SETDIGITS_L (n_l, I). Соответственно макросы
28 Криптография на Си и C++ в действии LSDPTRJ_(n_l) и MSDPTR_L(n_l) обеспечивают доступ к младшему и старшему разрядам п_1, причём каждый из них возвращает указатель на запрашиваемый разряд. Использование макросов, описанных в библиотеке flint.h, обеспечивает независимость от реального представления числа. Поскольку для натуральных чисел знак не нужен, у нас теперь есть все элементы, необходимые для представления этих чисел. Соответствующий тип данных мы определяем следующим образом: typedef unsigned short dint; typedef dint CLINT[CLINTMAXDIGIT + 1]; В соответствии с этим, большое число описывается так: CLINT n_J; Описание параметров функции типа CLINT следует из записи CLINT 2 n_l в заголовке функции. Определение указателя myptrj на CLINT- объект осуществляется посредством CLINTPTR myptrj или dint *myptr_l. В зависимости от установки константы CLINTMAXDIGIT в flint.h функции пакета FLINT/C могут обрабатывать числа длиной до 4096 бит, что соответствует 1223 десятичным разрядам или 256 разрядам по основанию 216. Изменяя CLINTMAXDIGIT, можно устанавливать требуемую максимальную длину. От этого параметра зависит определение других констант. Например, число USHORT-разрядов в CLINT-объекте задается следующим образом: #define CLINTMAXSHORT CLINTMAXDIGIT + 1 а максимальное число обрабатываемых двоичных разрядов определяется посредством #define CLINTMAXBIT CLINTMAXDIGIT « 4 Так как константы CLINTMAXDIGIT и CLINTMAXBIT часто используются, то для удобства будем обозначать их сокращенно МАХВ и МАХ2 (за исключением текста программ, где они будут приводиться в своём обычном виде). Из этого определения вытекает, что CLINT-объекты допускают целочисленные значения из промежутка [0, вМАХв -1], соответст- В связи с этим сравните главы 4 и 9 чрезвычайно интересной книги [Lind], где приводится подробное объяснение, в каких случаях в языке С векторы и указатели эквивалентны, а в каких - нет, и, что самое ^главное, к каким ошибкам может привести неправильное понимание этих случаев.
ГЛАВА 2. Числовые форматы 29 венно [О, 2МАХ2-1]. Обозначим величину ВМАХ" -1 = 2МАХ> -1 через iVmax- Это наибольшее натуральное число, представимое CLINT-объектом. Некоторым функциям приходится работать с числами, у которых разрядов больше, чем допускается CLINT-объектом. Для этих случаев определяются следующие варианты CLINT-типа: typedef unsigned short CLINTD[1+(CLINTMAXDIGIT«1)]; и typedef unsigned short CLINTQ[1+(CLINTMAXDIGIT«2)]; которые могут содержать в два (соответственно в четыре) раза больше разрядов. В качестве вспомогательного средства при программировании модуль flint.с определяет константы nulj, onej и twoj, которые представляют числа 0, 1 и 2 в CLINT-формате. В библиотеке flint.h есть соответствующие макросы SETZERO_L(), SETONE_L() и SETTWO_L(), которые устанавливают соответствующие значения CLINT-объектов.
ГЛАВА 3. Семантика интерфейса В словах понятия сокрыты. Гете, Фауст, часть 1 В дальнейшем мы установим основные свойства, касающиеся поведения интерфейса и использования функций пакета FLINT/C. Сначала мы рассмотрим текстуальное представление CLINT- объектов и FLINT/C-функций, но прежде хотелось бы прояснить некоторые основные принципы реализации, имеющие значение для использовании этих функций. Имена функций пакета FLINT/C оканчиваются на «J»; например, addj обозначает функцию сложения. Обозначения CLINT-объектов тоже оканчиваются на «J». Далее для простоты, если позволяют условия, мы будем приравнивать CLINT-объект п_1 тому значению, которое он представляет. Представление FLINT/C-функций начинается с заголовка, который содержит синтаксическое и семантическое описание интерфейса функции. Такие заголовки функций обычно выглядят следующим образом: Функция: Синтаксис: Вход: Выход: Возврат: Краткое описание функции int fj (CLINT a J, CLINT bj, CLINT c J); a_l, bj (операнды) cj (результат) 0, если все в порядке предупреждение или сообщение об ошибке в противном случае Здесь нужно различать между собой значения выхода и возврата. В то время как выход относится к значениям, которые функция хранит в переданных аргументах, под возвратом подразумеваются значения, которые функция возвращает посредством команды return. За исключением нескольких случаев (например, функции ld_l(), п. 10.3, и twofactJO, п. 10.4.1), значение возврата содержит сведения о состоянии или сообщения об ошибке. Другие параметры, не связанные с выходом, функция не изменяет. Обращения вида fj(aj, bj, aj), где aj и bj - аргументы и в конце
мг>- ,Г?' -0!^ .JV; -'i У. >тш иоа <f»>H> '4»V у i ')U-\ w,' 32 Криптография на Си и C++ в действии вычислений прежнее значение а_1 затирается результатом, вполне допустимы, так как результат записывается в возвращаемую переменную только после завершения операции. Применяя термин программирования на ассемблере, можно сказать, что в этом случае переменная а_1 используется как сумматор. Этот прием поддерживается всеми FLINT/C-функциями. В СLI NT-объекте п_1 имеются ведущие нули, если для некоторого ■!КХ£;>йл значения I выполняется следующее условие (DIGITS_L (n_l) == I) && (I > 0) && (п_1[1] == 0); Ведущие нули являются избыточными, так как они увеличивают длину представления числа, не влияя при этом на его значение. Однако в системе обозначений числа ведущие нули допустимы, поэтому их не следует просто игнорировать. Конечно, при реализации ведущие нули являются обременительной деталью, но они способствуют вводу данных с внешних источников и таким образом поддерживают стабильность всех функций. В пакете FLINT/C все функции понимают CLINT-числа с ведущими нулями, но не генерируют их. iv , Следующее положение относится к поведению арифметических функций в случае переполнения, которое происходит, если результат арифметической операции слишком велик для заданного типа. ^^шм^^щ.^^^^,. Хотя в некоторых публикациях по С говорится, что поведение I программы при переполнении зависит от реализации, стандарт \ языка С тем не менее точно регулирует случай переполнения { при арифметических операциях с беззнаковыми целыми типами I данных. Там утверждается, что следует применять арифметику 1 по модулю 2", когда тип данных представляет целые длиной п I бит (см. [Harb], п. 5.1.2). Соответственно, в случае переполнения ; основные арифметические функции, описываемые ниже, выдают : &"•'• результат, приведенный по модулю (Nmax+ 1), то есть остаток от . «^,^№ , ^ , целочисленного деления на Nmax + 1 (см. п. 4.3 и главу 5). В случае потери значимости (отрицательного переполнения), которая происходит при получении отрицательного результата, на выход выдается положительный вычет по модулю (Nmax + 1). Таким образом, FLINT/C-функции согласуются с арифметикой в соответствии со стандартом языка С. Если обнаруживается переполнение или потеря значимости, арифметическая функция возвращает соответствующий код ошибки. Этот код и другие коды ошибок, приведенные в таблице 3.1, описаны в заголовочном файле flint.h. .» i 11 л, jn, 41
ГЛАВА 3. Семантика интерфейса 33 Таблииа 3.7. Колы ошибок FLINT/C Кол ошибки E_CLINT_BOR Е CLINT DBZ E_CLINT_MAL E_CLINT_MOD E_CLINT_NOR E_CLINT_NPT E_CLINT_OFL E_CLINT_UFL Интерпретация Недопустимое основание в str2clint_l() (см. главу 8) Деление на нуль Ошибка при распределении ресурсов памяти Четные модули в умножении по Монтгомери Регистр недоступен (см. главу 9) Пустой указатель в качестве аргумента Переполнение Потеря значимости
ГЛАВА 4. Основные операции Таким образом, вычисления можно считать основой всех искусств. Адам Райе, Вычисления, 1574 А вы, бедные создания, совершенно бесполезны. Посмотрите на меня - я нужна всем. Эзоп, Ель и ежевика Для выполнения «матемагических» трюков в этой главе необходимо одно маленькое условие - вы должны знать таблицу умножения до 10... туда и обратно. Артур Бенджамин, Майкл Б. Шермер, Матемагия **l4t:*ч•' Любой вычислительный пакет программ составляется из функций, выполняющих основные арифметические операции: сложение, вычитание, умножение и деление. Производительность всего пакета держится на двух последних операциях, поэтому нужно очень аккуратно подходить к выбору и реализации соответствующих алгоритмов. К счастью, во втором томе классического «Искусства программирования для ЭВМ» Дональда Кнута можно найти большую часть из того, что требуется для этой части пакета FLINT/C. Прежде чем заниматься основными функциями, введем операцию сру_1(), копирующую один CLINT-объект в другой, и cmp_l(), сравнивающую размер двух CLINT-значений. Более строгое описание см. в п. 7.4 и в главе 8. Отметим, что в этой главе мы строим основные арифметические функции как единое целое, тогда как в главе 5 будет разумнее «посмотреть что у них внутри» и ввести ряд дополнительных операций: отбрасывание старших нулей, обработку переполнения и потери значащих разрядов, - сохраняя неизменными синтаксис и семантику этих функций. Но для понимания этой главы такие подробности не нужны - что ж, забудем пока о них.
36 Криптография на Си и C++ в действии 4.1. Сложение и вычитание Понятие «дальнейшие вычисления» означает: , ,,, ,, «к целому числу Пу прибавить целое число п2», ,.,,, ;, а результат этих дальнейших вычислений - целое число s - называется «результатом сло- v-S . г: ' жения» или «суммой r»i и п2» и записывается ni + п2. Леопольд Кронекер, Об идее чисел ; I Поскольку операции сложения и вычитания отличаются лишь зна- J ком, соответствующие алгоритмы практически одинаковы и могут быть рассмотрены одновременно. Пусть операнды а и b заданы в системе счисления с основанием В: т-\ I а:=(ат_хат_2...а0)в = ^а(В\ 0<я,<Я, /=о I !Л '■ 1=0 .1 и пусть для определенности а > Ь. Для операции сложения это ||| условие ни на что не влияет, поскольку слагаемые всегда можно поменять местами. Для операции вычитания оно означает, что разность является неотрицательным числом и, следовательно, может быть представлена CLINT-объектом без приведения по модулю #тах+1. ■' .v Вот основные шаги сложения. Алгоритм вычисления суммы а + Ъ 1 1. Положить i <г- 0 и с <— 0. 2. Вычислить t<r-cii + bi + с, s, <— tmod В не <r-\_t/BJ. 3. Положить / <— / + 1. При i<n-\ вернуться на шаг 2. . 4. Вычислить t <— я,- + с, si <— / mod Б и с <— Ь/#1 5. Положить i<r-i+ 1. При / < /я - 1 вернуться на шаг 4. 6. Положить sm <— с. 7. Результат: s = (^^.j.. .s0)B. На шаге 2 разряды слагаемых суммируются с переносом, при этом младшая часть результата записывается в разряд суммы, а старшая переносится в следующий разряд. Как только мы дойдем до старшего разряда одного из слагаемых, на шаге 4 все оставшиеся разряды
ГЛАВА 4. Основные операции 37 второго слагаемого складываются со всеми оставшимися переносами. До тех пор пока не обработан последний разряд слагаемого, младшая часть записывается в разряд суммы, а старшая переносится в следующий разряд. И, наконец, самый последний перенос (если он есть) записывается в самый старший разряд суммы. В случае вычитания, умножения и деления шаги 2 и 4 имеют тот же вид. Соответствующий код типичен для арифметических функций: 1 s = (USHORT)(carry = (ULONG)a + (ULONG)b + (ULONG)(USHORT)(carry » BITPERDGT)); Промежуточное значение /, участвующее в алгоритме, представлено переменной carry типа ULONG, в которую записывается сумма разрядов я,-, bi и переноса с предыдущей операции. Полученный новый разряд si записывается в младшую часть переменной carry и приведением типов преобразуется в значение типа USHORT. Возникающий перенос записывается в старшую часть переменной carry до следующего шага. При реализации этого алгоритма с помощью функции add_l() может возникнуть переполнение; в этом случае нужно привести сумму по модулю Nmax+ 1. Сложение int addj (CLINT a J, CLINT bj, CLINT sj); a J, bj (слагаемые) sj (сумма) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int addj (CLINT a_l, CLINT bj, CLINT sj) { dint ss_l[CLINTMAXSHORT + 1]; dint *msdptra_l, *msdptrb_l; dint *aptr_l, *bptrj, *sptr_l = LSDPTRJ. (ssj); ULONG carry = OL; int OFL = 0; Автор этого компактного выражения - мой коллега Robert Hammelrath. Функция: Синтаксис: Вход: Выход: Возврат:
38 Криптография на Си и C++ в действии В цикле сложения используются указатели. Проверяется, у какого из слагаемых число разрядов больше. Инициируются указатели aptrj и msdaptrj, соответствующие самому младшему и самому старшему разряду большего слагаемого (или а_1, если слагаемые имеют равную длину). Аналогично, указатели bptrj и msdbptrj соответствуют самому младшему и самому старшему разряду меньшего слагаемого, или Ь_1. Инициализация выполняется с помошью макросов LSDPTR_L() для младших разрядов и MSDPTR_L() для старших разрядов CLINT-объекта. Макрос DIGITS_L (a_l) определяет число разрядов CLINT-объекта а_1, а макрос SETDIGITSJ. (a_l, п) устанавливает число разрядов слагаемого а_1 равным п. if (DIGITS J. (aj) < DIGITS_L (bj)) { aptrj = LSDPTR_L (bj); bptrj = LSDPTR_L (aj); msdptraj = MSDPTR_L (bj); msdptrbj = MSDPTR_L (aJ); SETDIGITS_L (ssj, DIGITS_L (bj)); } else aptrj = LSDPTR_L (aj); bptrj = LSDPTR_L (bj); msdptraj = MSDPTR_L (aj); msdptrbj = MSDPTR_L (bj); SETDIGITS_L (ssj, DIGITSJ. (aj)); } На первом цикле процедуры addj разряды переменных aj и bj суммируются и записываются в результируюшую переменную ssj. Появление ведуших нулей не вызывает никаких проблем - они участвуют в вычислениях и отбрасываются при копировании результата в sj. Движение осуществляется от младших разрядов слагаемого b J к старшим разрядам. Это в точности соответствует школьному сложению в «столбик». Как и было обешано, используем перенос. while (bptrj <= msdptrbj) {
ГЛАВА 4. Основные операции 39 *sptrj++ = (USHORT)(carry = (ULONG)*aptrJ++ + (ULONG)*bptrJ++ + (ULONG)(USHORT)(carry » BITPERDGT)); } I Преобразуем USHORT-значения *aptr и *bptr путем приведения I типов в ULONG и сложим. К полученной сумме прибавим пере- I нос из преды душей итерации. Результатом является значение ти- I па ULONG, содержашее перенос в старшем слове. Это значение ,/^\ I присваивается переменной carry и хранится в ней до следующей ( .) I итерации. Из суммы берем младшее слово - это и есть результи- ) i I руюший разряд типа USHORT. Перенос, храняшийся в старшем W I слове carry, после сдвига на BITPERDGT и преобразования типов I участвует в следующей итерации. I На втором цикле оставшиеся разряды переменной а_1 складыва- I ются с переносом (если он есть); результат записывается в s_l. while (aptrj <= msdptraj) { *sptrj++ = (USHORT)(carry = (ULONG)*aptrJ++ + (ULONG)(USHORT)(carry » BITPERDGT)); } I Если на втором цикле возникает перенос, то результат будет на I один разряд длиннее, чем слагаемое а_1. Если результат превышает I I максимальное значение Nmax, допускаемое типом CLINT, то его I нужно привести по модулю (Nmax + 1) (см. главу 5), как это дела- I ется для стандартных беззнаковых типов. В этом случае возвра- I шается уведомление об ошибке E_CLINT_OFL. if (carry & BASE) { *sptrj = 1; SETDIGITSJ. (ssj, DIGITS_L (ssj) + 1); } if (DIGITS_L (ssj) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? */ { ANDMAX_L (ssj); /* Привести по модулю (Nmax + 1) */ OFL=E_CLINT_OFL;
40 Криптография на Си и C++ в действии } cpy_l (s_l, ss_l); f!N«»4, return OFL; } Временная сложность t всех представленных здесь процедур сло- Ч жения и вычитания равна t = О(п), то есть пропорциональна числу t 3 разрядов большего операнда. I t и Что ж, сложение мы рассмотрели, теперь перейдем к вычислению разности двух чисел а и Ь, заданных в системе счисления с основа- j нием В: { а = (ат.хат.г..,а0)в^b- (bn^bn.2-.Л0)в % Алгоритм вычисления разности а -Ъ 1. Положить / <г- О и с <— 1. 2. При с = 1 вычислить / <— # + я/ - /?,; иначе t <— В - 1 + а{? - 6/. 3. Положить j/ <— Г mod fincf- \_t/BJ. 4. Положить / <— / + 1. При / < п - 1 вернуться на шаг 2. I 5. При с = 1 вычислить t <— В + а,; иначе г <- 5 - 1 + а{. | 6. Вычислить si <— / mod В и с <— Ь/#1 7. Положить / <— / + 1. При i < т - 1 вернуться на шаг 5. 8. Результат: s = 0w_i *ш-2-. .s0)B. с '* "i,r Вычитание реализуется так же, как и сложение, за исключением следующего: • С помощью переменной carry типа ULONG мы «занимаем» из предыдущего старшего разряда уменьшаемого в том случае, если текущий разряд уменьшаемого меньше соответствующего разряда вычитаемого. • Здесь нужно следить уже не за переполнением, а за возможной потерей значащих разрядов; в этом случае результат вычитания вообще-то будет отрицательным; однако, поскольку тип CLINT (беззнаковый, нужно выполнить приведение по модулю (Nmax+1) (см. главу 5). Эта ситуация выявляется, когда функция возвращает код ошибки E_CLINTJJFL • И последнее: все ведущие нулевые разряды отбрасываются. Таким образом, получается такая функция вычисления разности чисел а I и b I типа CLINT.
ГЛАВА 4. Основные операции 41 функция: Синтаксис: Вход: Выход: Возврат: Вычитание int subj (CLINT aa_l, CLINT bbj, CLINT dj); aaj (уменьшаемое), bbj (вычитаемое) dj (разность) E_CLINT_OK, если все в порядке E_CLINT_UFL в случае потери значащих разрядов :0ft int subj (CLINT aaj, CLINT bbj, CLINT dj) { CLINT bj; dint a_l[CLINTMAXSHORT + 1]; /* Добавляем в a J еще один разряд */ dint *msdptra_l, *msdptrb_J; dint *aptrj = LSDPTFLL (aJ); dint *bptrj = LSDPTFLL (bj); dint *dptrj = LSDPTR_L (dj); ULONG carry = OL; int UFL = 0; cpyj (aj, aaj); cpyj (bj, bbj); msdptraj = MSDPTR_L (aj); msdptrbj = MSDPTRJ. (bj); Если aj < bj, то будем вычитать b_l не из aj, а из максимально возможного значения Nmax. К полученной разности прибавим (уменьшаемое + 1), то есть все вычисления выполняем по модулю (Nmax + D. Значение Nmax генерируем с помошью функции setmaxJO. if (LT_L (aj, bj)) { setmaxj (aj); msdptraj = aj + CLINTMAXDIGIT;
42 Криптография на Си и C++ в действии SETDIGtTS_L (d_l, CLINTMAXDIGIT); UFL=E_CLINTJJFL; } else { SETDIGITSJ. (dj, DIGITS_L (a_l)); } while (bptrj <= msdptrbj) { *dptr_l++ = (USHORT)(carry = (ULONG)*aptrJ++ - (ULONG)*bptrJ++ - ((carry & BASE) » BITPERDGT)); } while (aptrj <= msdptraj) { *dptr_l++ = (USHORT)(carry = (ULONG)*aptr_l++ - ((carry & BASE) » BITPERDGT)); } RMLDZRSJ. (dj); Складываем разность Nmax - b_l, записанную в dj, с величиной (уменьшаемое + 1). Результат: dj. if (UFL) { addj (dj, aaj, dj); incj (dj); } return UFL; } 0
ГЛА&44. Основные операции 43 Помимо рассмотренных функций add_l() и sub_l() введем еще две специальные функции, второй аргумент которых имеет тип USHORT, а не CLINT. Такие функции будем называть смешанными и обозначать префиксом «и», например uadd_J() и usub_l(). Преобразование значения типа USHORT в CLINT-объект осуществляется с помощью функции u2clint_l(), с которой мы познакомимся в главе 8. функция: Синтаксис: Вход: Выход: Возврат: Смешанное сложение переменных типа CLINT и USHORT int uaddj (CLINT a J, USHORT b, CLINT sj); aj, b (слагаемые) sj (сумма) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения Int uaddj (CLINT a J, USHORT b, CLINT sj) { int err; CLINT tmpj; u2clint_l (tmpj, b); err = addj (aj, tmpj, sj); return err; } Функция: Вычитание числа типа USHORT из числа типа CLINT Синтаксис: int usubj (CLINT a J, USHORT b, CLINT dj); Вход: aj (уменьшаемое), b (вычитаемое) Выход: dj (разность) результат: E_CLINT_OK, если все в порядке E_CLINT_UFL в случае потери значащих разрядов int usubj (CLINT aj, USHORT b, CLINT dj) { int err; CLINT tmpj;
44 Криптография на Си и C++ в действии u2clintj (tmpj, b); err = subj (a_l, tmpj, dj); return err; } Еще два важных частных случая сложения и вычитания: увеличение и уменьшение CLINT-объекта на 1 (инкремент и декремент). Реализуем их в виде функций inc_l() и dec_l() в виде процедуры- сумматора: новое значение операнда будем записывать на место старого - очень практичный способ, используемый во многих алгоритмах. Неудивительно, что реализация функций inc_l() и dec_l() осуществляется по аналогии с функциями add_l() и sub_J(). Они точно так же контролируют переполнение и потерю значащих разрядов, возвращая соответствующие коды ошибки E_CLINT_OFL и E_CLINT_UFL Функция: Синтаксис: Вход: Выход: Возврат: Увеличение CLINT-объекта на 1 intincj (CLINT a_l); а_1 (слагаемое) а_1 (сумма) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int incj (CLINT a J) { dint *msdptraj, *aptrj = LSDPTR_L (aj); ULONG carry = BASE; int OFL = 0; msdptraj = MSDPTR_L (aJ); while ((aptrj <= msdptraj) && (carry & BASE)) { *aptrj = (USHORT)(carry = 1UL + (ULONG)*aptrJ); aptr_l++; } if ((aptrj > msdptraj) && (carry & BASE))
ГЛАВА 4. Основные операции 45 { *aptrj = 1; SETDIGITS_L (a_l, DIGITS J- (a_l) + 1); if (DIGITS.L (a_l) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? */ { SETZERO_L (a_l); /* Привести по модулю (Nmax + 1) */ OFL=E_CLINT_OFL; } : ф&:*.,тП£ч return OFL; } Функция: Синтаксис: Вход: Выход: Возврат: Уменьшение CLINT-объекта на 1 int dec_l (CLINT a_l); aj (уменьшаемое) a J (разность) E_CLINT_OK, если все в порядке E_CL)IMT_UFL в случае потери значащих разрядов int dec_l (CLINT a_l) { dint *msdptra_l, *aptr_l = LSDPTR_L (aJ); ULONG carry = DBASEMINONE; if (EQZ_L (aj)) Л Потеря значащих разрядов ? */ { setmaxj (aj); /* Привести по модулю maxj */ return E_CLINT_UFL; } msdptraj = MSDPTR_L (aj); while ((aptrj <= msdptraj) &&
46 Криптография на Си и C++ в действии (carry & (BASEMINONEL « BITPERDGT))) { *aptr_l = (USHORT)(carry = (ULONG)*aptr_l - 1L); aptr_l++; } RMLDZRS_L (aJ); return E_CLINT_OK; } 4.2. Умножение Если все слагаемые пь п2, п3, ..., пг равны одному и тому же целому числу п, то сложение называется «умножением числа г на целое число п» и обозначается rij + п2 + п3 + ... + пг = гп. • Леопольд Кронекер, Об идее чисел Функция умножения играет основную роль в пакете FLINT7C, яв- 1 ляется одной из наиболее трудоемких и вместе с функцией деления определяет время работы многих алгоритмов. В отличие от сложе- \ ния и вычитания, с которыми мы уже знакомы, сложность класси- .* '.» , ческих алгоритмов умножения и деления - квадратичная от числа разрядов аргументов, недаром Дональд Кнут назвал одну из глав «Искусства программирования для ЭВМ» «Как быстро мы можем умножать?». На сегодняшний день в литературе опубликовано множество методов умножения больших и очень больших чисел, среди которых есть и весьма трудные. Одним из самых трудных является алгоритм, предложенный А. Шёнхаге и В. Штрассеном, в основе которого лежит быстрое преобразование Фурье над конечными полями. Время работы этого алгоритма оценивается величиной 0(п log n log log и), где п - число разрядов аргументов (см. [Knut], п. 4.3.3). Недостаток этого и подобных алгоритмов заключается в том, что выигрыш по скорости по сравнению с классическими методами сложности 0(п2) достигается лишь когда длина сомножителей составляет порядка 8000-10000 двоичных разрядов. До использования таких чисел к криптографии пока еще очень далеко. Сначала реализуем в пакете FLINT/C основной «школьный» метод умножения, основанный на «Алгоритме М» Кнута (см. [Knut], п. 4.3.1), и попробуем «выжать» из него максимальную скорость. Затем плотно займемся возведением в квадрат - многообещающей
гЛАВА 4. Основные операции 47 операцией в смысле снижения вычислительных затрат - и для обоих случаев попробуем применить алгоритм Карацубы, асимптотически более быстрый, чем 0(п2).2 Алгоритм Карацубы весьма любопытен и привлекателен своей кажущейся простотой, так что желающие могут заняться его реализацией как-нибудь (дождливым) воскресным днем. А пока мы посмотрим, дает ли что-либо этот алгоритм для библиотеки FLINT/C. 4.2.1. Школьный метод Рассмотрим умножение чисел а и Ь, заданных в системе счисления с основанием В: а'=(ат-1ат-2---ао)в =1X5'' Q < а, < Я, /•=0 /=0 Как нас учили в школе, произведение аЪ можно вычислить по схеме рис. 4.1 (для т = п = 3). Рисунок 4.7. Вычисления при умножении + + С22 (Р5 (а2а1а0)ь Рп Р4 1 ' С20 Р2\ Р\1 Ръ (bib\bQ)B P2Q Рп Р02 Р2 Рю Poi Pi Poo Ро)в Сначала вычисляем частичные произведения (а2а\ао)в ' fy для j = 0, 1, 2: значения aty - это младшие разряды значения {atbj + перенос), где ciibj - внутреннее произведение, а с2; - старшие разряды значения p2j. Затем суммируем частичные произведения и получаем произведение/7 = (р5Р4РзР2Р1Ро)в- В общем случае произведение р = аЪ равно п-\ w-1 У=0 /=0 Произведение двух операндов длины т и п имеет длину не менее т + п - 1 и не более т + п разрядов. Число элементарных умножений (в которых операнды меньше основания В) равно тп. Когда про алгоритм говорят, что он асимптотически более быстрый, это означает, что чем больше входные значения алгоритма, тем больше заметно увеличение скорости. Однако не следует преждевременно впадать в эйфорию - для наших целей это улучшение может вообще не играть никакой роли.
48 Криптография на Си и C++ в действии <щ®РЖР1 Функцию умножения можно реализовать по указанной схеме, то есть сначала вычислим и запомним все частичные произведения, а затем просуммируем их, домножая на соответствующую степень основания В. Этот школьный метод вполне годится для вычисления с карандашом и бумагой, но для компьютерной реализации он слишком громоздкий. Более эффективная альтернатива - сразу прибавлять внутренние произведения aty и переносы с с предыдущих шагов к результирующему разряду pi+j. Итоговое значение для каждой пары (/,/) записываем в переменную г: t^pi+j + ciibj + c, где t представимо в виде t = kB + l, 0<к, 1<В. *'i.£***#■ Тогда '*'" pi+j + atbj + c<B-\ + (B- 1)(Д - 1) + В - 1 = =(В - 1)В + В - 1 = В2 - 1 < В2. jr ЯП ». Текущие значения разрядов результата определяются из представления переменной t присваиванием pi+j <— /. Выполняем новый ,f: перенос: с <— к. Таким образом, теперь наш алгоритм умножения включает только внешний цикл, вычисляющий частичные произведения ai(bn_xbn-2... Ьо)в, и внутренний цикл, вычисляющий внутренние произведения щЬр где j = 0, ..., п - 1, и значения t и pi+j. Вот этот алгоритм. Алгоритм умножения 1. Для/ = 0, ...,/г-1 положить/?,<—(). 2. Положить / <— 0. 3. Положить у <— 0 не <— 0. 4. Положить t <— pi+j + ajb} + с,pi+j<— t mod В ис <r-\_tfBJ. 5. Положить^' <— j + 1. Приу < n - 1 вернуться на шаг 4. 6. Положить /?/+„ <— с. 7. Положить / <— / + 1. При / < т - 1 вернуться на шаг 3. 8. Результат: р = (рт+п-\Рт+п-2. • .ро)д. Этот основной цикл является ядром следующей реализации алгоритма умножения. В соответствии с полученными оценками, на шаге 4 для переменной t требуется точное представление чисел, меньших В2. Так же как и в случае сложения, представим внутренние произведения t типом ULONG. Заметим, что переменная / используется
ГЛАВА 4. Основные операции 49 неявно, а разряды произведения pi+j и переносы с выделяются внутри одного и того же выражения так же, как это делалось в функции сложения (см. стр. 37). Начальные значения будем задавать более эффективной процедурой, чем на шаге 1 алгоритма. функция: Синтаксис: Вход: Выход: Возврат: Умножение int mulj (CLINT f1_l, CLINT f2J, CLINT ppj); f1_l, f2_l (сомножители) ppj (произведение) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int mulj (CLINT f1_l, CLINT f2_l, CLINT ppj) { register dint *pptr_l, *bptr_l; CLINT aaj, bbj; CLINTD pj; dint *aj, *b_l, *aptrj, *csptr_l, *msdptra_l, *msdptrbj; USHORT av; ULONG carry; intOFL = 0; -f ). Сначала опишем переменные: результат будем записывать в pj, то есть эта переменная должна быть двойной длины. Сначала рассмотрим случай, когда один из сомножителей (а значит и произведение) равен нулю. Иначе заносим сомножители в aaj и bbj и убираем ведущие нули. if (EQZ_L (f1_l) || EQZ_L (f2_l)) { SETZERCLL (ppj); return E CLINT.OK; cpyj (aaj, f1J); cpyj (bbj, f2J);
50 Криптография на Си и C++ в действии В соответствии с описанием задаем указателям а_1 и Ь_1 адреса аа_1 и bb_l. Если число разрядов в аа_1 меньше, чем в bb_l, выполняем логическую перестановку: указатель а_1 всегда соответствует операнду с большим числом разрядов. if (DIGITS_L (aaj) < DIGITS_L (bb_l)) { a J = bbj; bj = aaj; } else { a_l = aaj; bj = bbj; } msdptraj = aj + *aj; msdptrbj = bj + *bj; wsm Аля экономии времени не выполняем инициализацию, указанную выше, а вычисляем циклически частичное произведение (b^b,^... Ь0)в • а0 и заносим результат в рп, рп_,, ..., рп. carry = 0; av = *LSDPTRJ_ (aj); for (bptrj = LSDPTR_L (bj), pptrj = LSDPTR_L (pj); bptrj <= msdptrbj; bptrj++, pptrj++) ^WltfVMMWW'MMM I *pptrj = (USHORT)(carry = (ULONG)av * (ULONG)*bptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); *pptrj = (USHORT)(carry » BITPERDGT); Дальше идет вложенный цикл умножения, начиная с разряда aj[2] переменной aj.
ГЛАВА 4. Основные операции 51 for (csptrj = LSDPTR_L (pJ) + 1, aptrj = LSDPTR_L (aJ) + 1; aptrj <= msdptraj; csptrj++, aptr_l++) { carry = 0; av = *aptr_l; for (bptrj = LSDPTFLL (bj), pptrj = csptrj; bptrj <= msdptrbj; bptr_l++, pptr_l++) { *pptrj = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l + (ULONG)*pptr_l + (ULONG)(USHORT)(carry » BITPERDGT)); } *pptr_l = (USHORT)(carry » BITPERDGT); } Максимально возможная длина результата равна сумме длин a_l и b_l. Случай, когда длина результата оказывается на единицу меньше, выявляется макросом RMLDZRS_L. SETDIGITS_L (p_l, DIGITS_L (aj) + DIGITS_L (bj)); RMLDZRSJ. (pj); Если результат превышает допустимые размеры для объектов типа CLINT, то он приводится по модулю (Nmax + 1), а флагу ошибки OFL присваивается значение E_CLINT_OFL. Приведенный по модулю результат записывается в pp_l. if (DIGITSJ- (pj) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? */ { ANDMAX_L (pj); /* Привести по модулю (Nmax +1)7 / OFL = E_CLINT_OFL; } .; i i яг в''
52 Криптография на Си и C++ в действии сру_1 (ppj, р_1); return OFL; } Время t = 0(тп) выполнения умножения пропорционально произведению длин т и п операндов. Для умножения, как и для сложения, можно реализовать смешанную функцию, первый аргумент которой имеет тип CLINT, а второй - тип USHORT. Укороченная версия CLINT-умножения требует 0(п) процессорных умножений. Однако этим результатом мы обязаны не какому-то усовершенствованию алгоритма, а просто малой длине USHORT-аргумента. Мы еще вернемся к этой функции, когда будем возводить в степень число типа USHORT (см. главу 6, функцию wmexpJQ). Для реализации функции umul_() воспользуемся слегка модифицированной функцией mul_l(). Функция: Синтаксис: Вход: Выход: Возврат: Умножение CLINT-объекта на число типа USHORT int umulj (CLINT aa_l, USHORT b, CLINT ppj); aaj, b (сомножители) ppj (произведение) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int umulj (CLINT aaj, USHORT b, CLINT ppj) { register dint *aptr_l, *pptrj; CLINT aj; CLINTD pj; dint *msdptra_l; ULONG carry; int OFL = 0; cpyj (aj, aaj); if (EQZ_L(aJ)||0==b) {
ГЛАВА4* Основные операции 53 SETZERCLL(ppJ); return E_CLINT_OK; } Предварительная подготовка завершена, теперь в иикле выполняем умножение CLINT-аргумента на USHORT-аргумент, перенос записываем в старший USHORT-разряд CLINT-аргумента. msdptraj = MSDPTR_L (a_l); carry = 0; for (aptrj = LSDPTFLL (aj), pptrj = LSDPTRJ (pj); aptrj <= msdptraj; aptr_l++, pptr_l++) { ^<- *pptr_l = (USHORT)(carry = (ULONG)b * (ULONG)*aptr_l + "L (ULONG)(USHORT)(carry » BITPERDGT)); } *pptr_l = (USHORT)(carry » BITPERDGT); SETDIGITS_L (pj, DIGITS_L (aj) + 1); RMLDZRS_L (pj); if (DIGITS_L (pj) > (USHORT)CLINTMAXDIGIT) '•"!"Ш8. ош>:. /* Переполнение ? */ { ANDMAX_L (pj); /* Привести по модулю (Nmax + 1) */ OFL = E_CLINT_OFL; } cpyj (pp_l, pj); return OFL; }
54 Криптография на Си и C++ в действии . 4»^"!*.>*tat •6ТН.Я*:М 4.2.2. А возведение в квадрат - быстрее Возведение большого числа в квадрат требует значительно меньнг го числа умножений, чем умножение двух больших чисел, благодаря симметрии операндов. Это наблюдение становится еще более важным, когда дело доходит до возведения в степень, и нам требуется не одно, а сотни возведений в квадрат - тогда можно получить значительное увеличение скорости. Снова обратимся к хорошо известной схеме умножения, на этот раз с двумя одинаковыми сомножителями (a2aiao)B (см. рис. 4.2). Рисунок 4.2. Вычисления при возвелении в квалрат (a2aia0)B • (a2ala0)B + + а&г а2а{ а\а2 а2а0 ахах а0а2 а\а0 a0cii а<Ро (Р5 Р4 Ръ Р2 Pi Ро)в Заметим, что внутренние произведения ащ вычисляются, во- первых, однократно для i=j (выделены полужирным шрифтом на рис. 4.2), и, во-вторых, дважды для i Ф] (на рисунке они обведены прямоугольниками). Таким образом, вместо девяти умножений можно выполнять всего три, удвоив слагаемые aiujBl+J при i<j. Тогда сумму внутренних произведений при возведении в квадрат можно переписать как р = fafljB» = 2± §в|вув*' + %>Вг>. i,j=0 /=0 j=i+l ;=0 Таким образом, число элементарных умножений по сравнению со «школьным» методом сокращается с п2 до п(п + 1)/2. Вычисление последнего выражения для р естественно алгоритмически реализовать в виде двух вложенных циклов. Алгоритм 1 возведения в квадрат 1. Для / = 0, ..., и - 1 положить pi <— 0. 2. Положить / <г- 0. ' 3. Положить t <— p2i + я Д p2i <— t mod Вис*- lt/BJ. 4. Положить у <— i + 1. При j = n -1 перейти на шаг 7. 5. Положить t <— pi+j + 2djCij + с, pi+J- <— t mod В и с <— UlB]. 6. Положить у <— j + 1. При у < п - 1 вернуться на шаг 5. 7. Положить Pi+n <— с.
ГЛАВА 4. Основные операции 55 8. Положить i<r- i+ 1. При / < п - 1 вернуться на шаг 3. 9. Результат: р = (pm-iPin-i- • -Ро)в- Выбирая типы данных для представления переменных, следует учесть, что t может принимать значение (В - 1) + 2{В - I)2 + (В - 1) = 2В2 - 2В. (на шаге 5 алгоритма). Это означает, что для представления t в системе счисления с основанием В понадобится больше чем два разряда, поскольку В2 - 1 < 2В2 - 2В < 2В2 - 1, то есть типа ULONG для представления t будет недостаточно (из неравенства выше следует, что требуется еще один двоичный разряд). При реализации на Ассемблере это не вызывает никаких проблем, поскольку всегда можно воспользоваться процессорным разрядом переноса. Но в языке С все не так просто. Для разрешения проблемы будем на шаге 5 алгоритма 1 выполнять умножение на 2 в отдельном цикле. Тогда для шага 3 потребуется свой цикл. Небольшие усилия, затраченные на разбирательство с циклами, окупятся появлением дополнительного двоичного разряда. Вот измененный алгоритм. Алгоритм 2 возведения в квадрат 1. Инициализация: для i = 0, ..., п - 1 положить р{ <— 0. 2. Вычисление произведения разрядов с неравными индексами: положить / <г- 0. 3. Положить^' <— / + 1 и с <г- 0. 4. Положить t <— pi+j + а{а} + с,pi+jf- tmod В не <r-[_t/B]. 5. Положить у <— j + 1. При j <n-l вернуться на шаг 4. 6. Положить pi+n <— с. 7. Положить i<—i+ 1. При i<n~2 вернуться на шаг 3. 8. Удвоение внутренних произведений: положить i <— 1 и с <— 0. 9. Положить t <— 2pi + с, pi <r- t mod В ис <r- it/BJ. 10. Положить i<—i+ 1. При i<2n-2 вернуться на шаг 9. 11. Положить р1п-\ <— с. 12. Суммирование внутренних квадратов: положить i <— 0 и с <— 0. 13. Положить / <-p2i + а2 + с,p2i <— t mod В и с <— Ь/в]. 14. Положить / <— р2/+1 + с» Pn+i <— * m°d 5 и с <— 15. Положить / <— / + 1. При i < n - 1 вернуться на шаг 13. 16. Положить р2п_х <- /?2л_1 + с. Результат: р = (рт-хРьг-г- • -А))д-
56 МЪЧ\*АЩ . Криптография на Си и C++ в действии При реализации на С вместо шага 1, по аналогии с умножением, вычисляем и запоминаем первое частичное произведение а0(ап,\ ап-г---а\)в> Функция: Синтаксис: Вход: Выход: Возврат: Возведение в квадрат int sqrj (CLINT f_l, CLINT ppj); f J (операнд) ppj (квадрат) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int sqrj (CLINT f_l, CLINT ppj) { register dint *pptr_l, *bptrj; CLINT aj; CLINTD pj; dint *aptr_l, *csptrj, *msdptraj, *msdptrb_l, *msdptrc_l; USHORT av; ULONG carry; int OFL = 0; '}-**• ъ w .^imi &, cpyj (aj, f J); if (EQZ_L (aj)) { SETZERO_L (ppj); return E_CLINT_OK; } msdptrbj = MSDPTR_L (aj); msdptraj = msdptrbj - 1; Инициализация результирующего вектора по указателю pptrj выполняется путем вычисления частичного произведения a0(an_ian_2... ai)e, по аналогии с умножением. Разряду р0 здесь не присваивается никакое значение; он должен быть нулевым. *LSDPTRJ_ (pj) = 0;
ГЛАВА 4. Основные операции 57 ОС i £)С< .$тт»' *»•<-' carry = 0; av = *LSDPTRJ_ (aj); for (bptrj = LSDPTR.L (a_l) + 1, pptrj = LSDPTR_L (pj) + 1; bptrj <= msdptrbj; bptrj++, pptr_l++) { *pptrj = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l + (ULONG)(USHORT)(carry » BITPERDGT)); } *pptrj = (USHORT)(carry » BITPERDGT); Цикл суммирования внутренних произведений a;aj. for (aptrj = LSDPTR_L (aj) + 1, csptrj = LSDPTR_L (pj) + 3; aptrj <= msdptraj; aptr_l++, csptrj += 2) { carry = 0; av = *aptrj; for (bptrj = aptrj + 1, pptrj = csptrj; bptrj <= msdptrbj; bptrj++, pptrj++) { *pptrj = (USHORT)(carry = (ULONG)av * (ULONG)*bptrJ + (ULONG)*pptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); *pptrj = (USHORT)(carry » BITPERDGT); msdptrcj = pptrj; f j | Умножение промежуточного результата из pptrj на 2 выполняем с помошью сдвига (см. также п. 7.1). carry = 0; for (pptrj = LSDPTR_L (pj); pptr_l <= msdptrcj; pptrj++) {
58 Криптография на Си и C++ в действии *pptr_l = (USHORT)(carry = (((ULONG)*pptr_l) « 1) + (ULONG)(USHORT)(carry » BITPERDGT)); } *pptrj = (USHORT)(carry » BITPERDGT); Теперь вычисляем «главную диагональ». carry = 0; for (bptrj = LSDPTR_L (aj), pptrj = LSDPTR_L (pj); bptrj <= msdptrbj; bptr_l++, pptr_l++) { *pptr_l = (USHORT)(carry = (ULONG)*bptr_l * (ULONG)*bptr_l + (ULONG)*pptr_l + (ULONG)(USHORT)(carry » BITPERDGT)); pptr_l++; *pptr_l = (USHORT)(carry = (ULONG)*pptr_l + (carry » BITPERDGT)); } Все остальное - так же, как в умножении. SETDIGITSJ. (pj, DIGITS_L (aj) « 1); RMLDZRSJ. (pj); if (DIGITS_L (pj) > (USHORT)CLINTMAXDIGIT) Л Переполнение? 7 { ANDMAX_L (pj); /* Привести по модулю (Nmax +1)7 OFL = E_CLINT_OFL; } cpyj (ppj, pj); return OFL;
ГЛАВА 4. Основные операции 59 Время работы процедуры возведения в квадрат равно 0(п2), то есть квадратичное от длины операнда. Однако, поскольку здесь требуется п(п + 1)/2 элементарных умножений, эта функция почти в два раза быстрее, чем умножение. 4.2.3. Поможет ли метод Карацубы? £И Дух умножения и деления разрушил все вокруг 'Я и устремился к отдельной части целого. Стэн Надольный, Бог дерзости Как мы и обещали, рассмотрим метод умножения, носящий имя русского математика А. Карацубы, опубликовавшего несколько вариантов этого метода (см. [Knut], п. 4.3.3). Пусть числа а и Ь - натуральные длины п = 2к разрядов в системе счисления с основанием В. Представим а и Ъ в системе счисления с основанием Вк: а = (ciido) ь Ъ - (Ьфо) к. Если умножать а на b традиционным спосо- -' бом, то для вычисления произведения ab = В2каф{ + Вк(аф\ + аф0) + аф^, требуется четыре умножения по основанию Вк и, следовательно, п2 = 4к2 элементарных умножений по основанию В. Однако если положить с0 := а0Ь0, 1 c2:=(ao + alXbo + bl)-co-cu Л*: то '■■У1 ab = Вк(Вкс{ + с2) + с0. Оказывается, теперь для вычисления произведения ab нужно всего три умножения чисел по основанию Вк или, что то же самое, Ък умножений по основанию В плюс несколько операций сложения и сдвига (умножение на Вк можно реализовать сдвигом на к разрядов в системе счисления с основанием В', см. п. 7.1). Предположим, что число п разрядов сомножителей а и b является степенью числа 2. Тогда, рекурсивно применяя указанную процедуру для вычисления частичных произведений, можно свести алгоритм к выполнению только элементарных умножений по основанию В. Откуда получаем 3log2/7 =/z,og23 =/i1,585 элементарных умножений вместо п2 (в классическом методе); дополнительное время потребуется еще для операций сложения и сдвига. Применительно к возведению в квадрат этот процесс несколько упрощается. Если с0 := а0 ,
60 Криптография на Си и C++ в действии •$лт ,UH-*F..' с\ := а{ , сг := (а0 + а{) -с0-сь то я2 = £*(£*cj + с2) + с0. На нас работает еще и то, что при возведении в квадрат оба сомножителя всегда имеют одну и ту же длину, что далеко не всегда выполняется в случае умножения. Следует, однако, помнить, что рекурсия тоже требует определенных затрат времени, так что надеяться на какие-либо преимущества перед классическим методом, не обремененным рекурсией, можно лишь при работе с большими числами. Чтобы иметь полную информацию о реальной производительности алгоритма Карацубы, рассмотрим функции kmul() и ksqr(). Деление сомножителей на две части выполняется на месте, то есть копировать их не нужно. А вот что нужно, так это, во-первых, снабдить сомножители указателями на младшие разряды и, во-вторых, делать это для каждого сомножителя отдельно (так как длины сомножителей могут различаться). В качестве эксперимента мы реализовали смешанную функцию, объединившую в себе рекурсивную процедуру для умножения чисел, длина которых превышает некоторое число, определяемое макросом, и обычное умножение и возведение в степень для маленьких чисел. Для нерекурсивного умножения в функциях kmul() и ksqr() будем использовать вспомогательные функции mult() и sqr(), в которых умножение и возведение в квадрат реализовано в виде базовых (kernel) функций, не поддерживающих тождественные адреса аргументов (режим сумматора) и приведение по модулю в случае переполнения. Функция: Метод Карацубы умножения двух чисел а_1 и Ь_1 длиной 2к разрядов в системе счисления с основанием В | Синтаксис: void kmul(clint *aptr_l, dint *bptr_l, int len_a, int len_b, CLINT pj); | Вход: aptrj (указатель на младший разряд сомножителя а_1) bptrj (указатель на младший разряд сомножителя Ь_1) 1еп_а (число разрядов сомножителя а_1) len_b (число разрядов сомножителя Ь_1) I Выход: р_| (произведение) void kmul (dint *aptr_l, dint *bptr_l, int len_a, int len_b, CLINT pj) {
ГЛАВА 4. Основные операции 61 ■1М»- CLINT c01J,c10J; dint cO_l[CLINTMAXSHORT + 2] dint c1 J[CLINTMAXSHORT + 2] dint c2J[CLINTMAXSHORT + 2] CLINTD tmpj; dint *a1ptr_l, *b1ptr_l; int 12; if ((len_a == len_b) && (len_a >= MUL_THRESHOLD) && (0==(len_a&1)) ) { о Если оба сомножителя имеют одно и то же четное число разрядов, превышающее значение MUL_THRESHOLD, то используем рекурсию, разбивая сомножители на две половины, младшим разрядам каждой из которых соответствуют указатели aptrj, а1 ptrj, bptrj, Ы ptr_l. Эти половины мы не копируем и тем самым экономим время. Значения с0 и с, вычисляются рекурсивным вызовом функции kmul() и присваиваются переменным cO_l ис1 I типа CLINT. 12 = len_a/2; a1ptr_l = aptr_l + l2; MptrJ = bptrj+ I2; kmul (aptrj, bptrj, 12,12, cOJ); kmul (alptrj, blptrj, 12,12, d J); о При вычислении значения с2 = (а0 + at)(b0 + bt) - с0 - Cj выполняем два сложения, один вызов функции kmulO и два вычитания. Аргументами вспомогательной функции addkar() являются указатели на младшие разряды и число разрядов двух слагаемых равной длины, значением функции - сумма этих слагаемых, имеющая тип CLINT. addkar (alptrj, aptrj, 12, c01 J); addkar (blptrj, bptrj, 12, c10J);
62 Криптография на Си и C++ в действии kmul (LSDPTR.L (c01_l), LSDPTFLL (с10_1),' DIGITS_L (c01_l), DIGITS_L (c10_l), c2_l); к sub (c2_l, c1_l, tmpj); sub (tmpj, cOJ, c2J); Выполнение функции заканчивается вычислением значения Вк(ВкСт + с2) + с0. Аля этого используем функцию shiftaddO, которая при сложении сдвигает влево первое из двух слагаемых типа CLINT на заданное число позиций в системе счисления с основанием В. shiftadd (c1_l, c2_l, I2, tmpj); shiftadd (tmpj, cOJ, 12, pj); } Если хотя бы одно из входных условий не выполнено, прерываем рекурсию и вызываем нерекурсивную функцию умножения mult(). Для вызова функции mult() необходимо перевести части aptrj и bptrj в формат CLINT. else { memcpy (LSDPTRJ. (d J), aptrj, len_a * sizeof (dint)); memcpy (LSDPTFLL (c2J), bptrj, len_b * sizeof (dint)); SETDIGITSJ- (d J Jen_a); SETDIGITSJ- (c2J, len_b); mult(c1J, c2J, pj); RMLDZRS_L (pj); } } Возведение в квадрат методом Карацубы выполняется аналогично, поэтому не будем его описывать подробно. Для вызова функций kmul() и ksqr() используем функции kmulJO и ksclrJ(), имеющие стандартный интерфейс. I
ГЛАВА 4. Основные операции 63 функция: Умножение и возведение в квадрат методом Карацубы Синтаксис: int kmulj (CLINT a J, CLINT b_l, CLINT pj); int ksqrj (CLINT aj CLINT pj); Вход: a J, bj (сомножители) Выход: pj (произведение или квадрат) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения Функции, реализующие алгоритмы Карацубы, читатель найдет в файле kmul.c на прилагаемом к книге компакт-диске. Многочисленные проверки этих функций (на Pentium III, 500 МГц, под Linux) показали, что наилучший результат достигается, когда нерекурсивная процедура умножения вызывается для не более чем 40-разрядных чисел (что соответствует 640 двоичным разрядам). Временные оценки для нашей программы приведены на рис. 4.3. 50 S 40 30 о о о S 20 < 5* 10 U о I hO 1 г о □ —I—I—г—г I— ■ * Щ 1 1 Л~ 1000 2000 3000 Двоичные разряды 4000 5000 Рисунок 4.3. Процессорное время умножения метолом Карацубы Из рисунка видно, что результаты действительно оправдали наши ожидания. Разница между обычным умножением и возведением в степень составляет около 40%. Для чисел размером более 2000 двоичных разрядов время работы алгоритмов становится более заметным, лидирует по скорости алгоритм Карацубы. Интересный факт: «нормальное» возведение в квадрат sqr_l() значительно быстрее умножения Карацубы, а возведение в квадрат методом Карацубы ksqr_l() лидирует только для чисел длиной более 3000 двоичных разрядов.
64 Криптография на Си и C++ в действии Функции, реализующие алгоритмы Карацубы для маленьких чисел, значительно ускорены по сравнению с первым изданием этой книги, но в них все еще есть что улучшать. Заметные скачки во времени работы функции kmul_l() показывают, что рекурсия прерывается раньше, чем это определено пороговым значением, если длина сомножителей - нечетное число. В худшем случае это происходит в самом начале процедуры умножения, и даже для очень больших чисел нам не остается ничего лучшего, как применять обычное умножение. Как нам кажется, стоит обобщить функции, реализующие алгоритм Карацубы так, чтобы они могли обрабатывать аргументы разной (в том числе нечетной) длины. Дж. 3 игл ер (J. Ziegler) [Zieg] из Института Марка Планка (г. Саарбрюкен, Германия) разработал для 64-разрядного процессора (Sun Ultra-1) переносимую программу, которая реализует алгоритмы умножения и возведения в степень методом Карацубы и «обгоняет» обычные методы на числах длины 640 двоичных разрядов. Возведение в квадрат работает на 10% быстрее для 1024-битных чисел и на 23% - для 2048-битных. Еще раз отметим, что алгоритмы Карацубы в том виде, в каком они есть, не дают значительного преимущества для криптографических приложений, так что мы предпочитаем вернуться к традиционным функциям mulj() и sqr_J() умножения и возведения в степень. Если для ваших приложений функции, реализующие алгоритмы Карацубы, подходят, просто воспользуйтесь ими, а не функциями mulJO и sqr_l(). 4.3. Деление с остатком - У тебя осталось что-нибудь в кармане? - Нет, - отвечала Алиса грустно. - Только наперсток. Льюис Кэррол, Приключения Алисы в Стране Чудес, (Перевод с английского Н. Демуровой) Заложим в наше здание основных арифметических операций над большими числами последний камень - деление - наиболее трудную из всех операций. Поскольку мы работаем с натуральными числами, то и результат мы можем выражать только натуральными числами. Принцип деления, которым мы собираемся заняться, называется делением с остатком и основан на следующем соотношении. Для данных чисел a, b e Ж, Ь>0, существует единственная пара целых чисел q и г таких, что a- qb + г, где О < г < Ь. Будем называть q частным, а г остатком от деления а на Ь. Чаще всего нас интересует только остаток, так что о частном можно не беспокоиться. В главе 5 мы узнаем, как важно уметь вычис-
ГЛАВА 4. Основные операции 65 Рисунок 4.4. Схема леления с остатком лять остаток - эта операция используется о многих алгоритмах, всегда одновременно со сложением, вычитанием, умножением и возведением в степень. Так что нам нужно постараться разработать как можно более быстрый алгоритм. Самый простой способ деления с остатком для натуральных чисел awb- вычитать делитель b из делимого я, пока остаток г не будет меньше делителя. Подсчитав число вычитаний, мы получим частное. Частное q и остаток г равны: q = \jalb\ wr-a- la/b]b.3 Согласитесь, делить с остатком с помощью вычитания очень скучно. Даже в школьном методе деления «в столбик» используется значительно более эффективный алгоритм: разряды частного определяются последовательно, делитель умножается на каждый из них, а полученные частичные произведения вычитаются из делимого. Пример работы этого алгоритма приведен на рис. 4.4. 354938:427=831, остаток 101 - 341 6\И = 01333 - 1281J = 00528 - 422 = 101 м'- л rt'S!! Уже при вычислении первого разряда частного - 8 - нам нужно попытаться угадать его или определить методом проб и ошибок. Ошибка выявляется либо если произведение (разряда частного на делитель) слишком велико (в нашем примере - больше, чем 3549), либо если разность разрядов делимого и частичного произведения больше, чем делитель. В первом случае выбрано слишком большое число (разряд частного), во втором - слишком маленькое; как бы то ни было, придется его исправлять. При разработке программы эвристический образ действия следует заменить чем-нибудь более определенным. Попробуем же, вслед за Д. Кнутом (см. [Knut], п. 4.3.1), «отшлифовать» наши грубые вычисления. Обратимся к нашему примеру. Пусть натуральные числа a = (ат+п-\ат+п-2-.-ао)в и b = (Ьп.\Ьп-2---Ьо)в представлены в системе счисления с основанием В и Ьп-\ > 0 (старший разряд). Будем искать частное q и остаток г такие, что a- qb + г, где 0 < г < Ь. Действуя согласно методу деления «в столбик», на каждом шаге получаем значение qj:=\_R/b]<B, где число R = (am+n-\am+„-2-.-ak)B образовано старшими разрядами делимого, а значение к выбрано из Заметим, что для а < 0, когда q = -[\a\/b~] и г = b - (\а\ + qb), если а \ b , и г = 0, если а \ Ь, деление с остатком сводится к случаю a, b e ИМ.
66 Криптография на Си и C++ в действии условия 1 < [Л1Ь\ (в примере выше на первом шаге получаем т + л-1 = 3 + 3-1= 5, Л = 2, Я = 3549). Далее полагаем R:=R-qp\ разряд qj частного определен правильно, если выполнено условие 0<R<b. Теперь заменяем R на сумму RB + (следующий разряд делимого) и вычисляем следующий разряд частного как [_R/b]. После того как пройдены все разряды делимого, процесс останавливается. Остаток от деления равен последнему найденному значению R. Чтобы запрограммировать эту процедуру, нам необходимо уметь для заданных больших чисел R = (rnrn_i...r0)B и b = (^„-А-г-.-^оЪ таких, что \_RIb] < В, находить частное Q := \_RIb] (разряд гп может быть и нулевым). Воспользуемся аппроксимацией q для Q, вычисляемой по старшим разрядам числа R и В, из книги Кнута. Пусть (4.1) q := mm< r„B + rt /2-1 rjn-\ B-l Если bn_\ > [_R/b], то q удовлетворяет двойному неравенству (см. [Knut], п. 4.3.1, Теоремы А и В): q ~2<Q< q. В предположении, что старший разряд делителя достаточно велик || по сравнению с В, аппроксимация q превышает истинное значение Q не больше чем на 2 и никогда не бывает слишком мала. Этого всегда можно добиться, «растянув» операнды а и Ь. Выберем число d > 0 так, чтобы dbn_{ > [_B/2J и положим a:=ad = (am+ndm+n_l...a0)B, b := bd = Фп-\К-2--А)в • Значение d выбираем так, чтобы число разрядов в b не превышало числа разрядов в Ь. При этом мы учли, что а может содержать на один разряд больше, чем а (если это не так, полагаем ат+п - 0). Как бы то ни было, значение d лучше выбрать равным степени двойки, поскольку в этом случае «растягивание» операндов осуществляется простым сдвигом. Так как оба операнда умножаются на одно и то же число, частное не изменится: [a/b\= [a/bj. Прежде чем применять аппроксимацию q из формулы (4.1) к «растянутым» операторам а (соответственно г) и b , уточним ее, чтобы получить q = Q или q = Q +1 : если для выбранного значения q выполняется неравенство bn_2q > (rnB + rn_{ -qbn_x)B + rn_2 , то уменьшаем q на 1 и снова проверяем выполнение неравенства. Так мы отсеиваем все случаи, когда q превышает истинное значение на 2;
ГЛАВА 4. Основные операции 67 Функция: Синтаксис: Вход: Выход: Возврат: в очень редких случаях q будет превышать истинное значение на 1 (см. [Knut], п. 4.3.1, Упражнения 19, 20). Последняя ситуация выявляется, когда мы вычитаем частичное произведение делителя на разряд частного из того, что осталось от делимого. В этом случае в последний раз уменьшаем q на 1 и корректируем остаток. Приведем теперь алгоритм деления с остатком. Алгоритм деления с остатком числа а = (am+n^iam+n^2^»ao)B > 0 на число Ь = Фп-Фп-ъ^Ь^в > 0 1. Определить множитель d как описано выше. 2. Положить г := (rm+nrm+n.{rm+n.2...r0)B <- (0ат+п_{ат+п.2...а0)в. 3. Положить / <— т + nj <— пи 4. Положить q <— minJ' п +п~ L 5 - Н , где rt, /7—1» */i-i - разряды соответствующих векторов, умноженных на d (см. выше). Если bn_2q > (rtB + rM - qbn_x)B + rt_2 , то положить q <r- q -1 и повторить проверку. 5. Если г - bq < 0, то положить q <— q -1. 6. Положить г := for^.. ,г^)д <- (r/r^i.. .г,_„)я - bq и qj <r-q . 7. Положить / <— / — 1 и у <— У — 1. При / > п вернуться на шаг 4. 8. Результат: q = {qmqm.x.. ,q0)B nr= (rn_xrn__2.. ,r0)B. Если делитель состоит всего из одного разряда b0i то процедуру можно сократить, задав в самом начале г <— 0 и поделив двухразрядное число (га,)д с остатком на Ь0. Тогда в г записывается остаток: г <r~ (rdi)B - qfio, а щ пробегает все разряды делимого. По окончании процедуры остаток будет равен г, а частное -q = {qm4m-\• • -Qo)b- Теперь, когда у нас есть все необходимое для реализации деления, напишем на языке С функцию, соответствующую рассмотренному выше алгоритму. Деление с остатком int divj (CLINT d1J, CLINT d2J, CLINT quotj, CLINT rem J); d1 J (делимое), d2_l (делитель) quotj (частное), remj (остаток) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на О
68 Криптография на Си и C++ в действий ■((,*» int divj (CLINT d1_l, CLINT d2_l, CLINT quotj, CLINT remj) { register dint *rptr_l, *bptr_l; CLINT bj; /* Допускаем остаток двойной длины плюс 1 разряд */ dint r_l[2 + (CLINTMAXDIGIT « 1)]; dint *qptr_l, *msdptrb_l, *lsdptrr_l, *msdptrr_l; USHORT bv, rv, qhat, ri, ri_1, ri_2, bn, bn_1; ULONG right, left, rhat, borrow, carry, sbitsminusd; unsigned int d = 0; int i; Присваиваем значения делимого а — (ат+п_1ат+п_2...ао)в и делителя b = (Ьп_,Ьп_2...Ьо)в переменным r_l и b_l типа CLINT. Отбрасываем все ведушие нули. Если при этом делитель равен нулю, то завершаем функцию с кодом ошибки E_CLINT_DBZ. Алина делимого может достигать удвоенного числа разрядов, заданного в МАХВ. Позже это позволит нам использовать деление в функциях модульной арифметики. Аля хранения частного двойной длины следует выделить память, которая всегда должна быть доступна. cpyj (r_l, d1_l); cpyj (bj, d2_l); if (EQZ_L (bj)) return E_CLINT_DBZ; о Проверяем тривиальные ситуации: делимое = 0, делимое меньше делителя или делимое равно делителю. В любом из этих случаев завершаем процедуру. if (EQZ_L (r_l)) { SETZERO_L (quotj); SETZERO_L (remj); return E_CLINT_OK;
ГЛАВА 4. Основные операции 69 i = crnpj (r_l, b_l); if(i = -1) { cpyj (remj, rj); SETZERO_L (quotj); return E_CLINT_OK; } else if (I == 0) О П { SETONE_L (quotj); SETZEROJ. (remj); return E_CLINT_OK; } На следующем шаге проверяем, не состоит ли делитель всего из одного разряда. В этом случае строим ветвь более быстрого деления, которую мы опишем позже. if(DIGITS_L(bJ)==1) goto shortdiv; Теперь начинаем собственно деление. Сначала задаем множитель d как степень двойки. Пока bn_, > BASEDIV2 := LB/2.L сдвигаем старший бит bn_| делителя влево на один бит, при этом увеличиваем d каждый раз на 1 (начинаем с d = 0). Затем устанавливаем указатель msdptrbj на старший разряд делителя. Впоследствии мы будем часто пользоваться значением BITPERDGT-d, так что запишем его в переменную sbitsminusd. msdptrbj = MSDPTFLL (bj); bn = *msdptrb_l; while (bn < BASEDIV2) { d++; bn «= 1;
70 Криптография на Си и C++ в действии ■т f } sbitsminusd = (int)(BITPERDGT - d); ^^i Если d > 0, то вычисляем два старших разряда Ъп_хЪп_г числа db и записываем их в bn и bn_1. Здесь следует различать случаи, когда делитель b имеет ровно два разряда и больше чем два разряда. В первом случае справа в Ьп_2 дописываем двоичные нули, во втором случае младшими битами числа Ьп_2 становятся биты числа bn_3. if (d > 0) { bn += *(msdptrb_l - 1) » sbitsminusd; if (DIGITS_L (bj) > 2) { bn_1 = (USHORT)(*(msdptrbJ - 1) « d) + (*(msdptrbj - 2) » sbitsminusd); } else { bn_1 = (USHORT)(*(msdptrbJ - 1) « d); } } else bn_1 = (USHORT)(*(msdptrbJ - 1)); В CLINT-векторе rj, куда будем записывать остаток от деления, устанавливаем указатели msdptrrj и IsdptrrJ на старший и младший разряды числа (am+nam+n_l...am+1)B соответственно. Разряд am+n переменной r_l полагаем равным 0. Устанавливаем указатель qptrj на старший разряд частного. msdptrbj = MSDPTR_L (bj); msdptrrj = MSDPTR_L (rj) + 1; IsdptrrJ = MSDPTR_L (rj) - DIGITS_L (bj) + 1;
71 *msdptrr_l = 0; qptrj = quotj + DIGITS.L (rj) - DIGITS_L (bj) + 1 ; Переходим к основному циклу. Указатель IsdptrrJ пробегает разряды ат, ат_2, ..., а0 делимого в rj, а (неявный) индекс i - значения i = т + п, ..., п. while (IsdptrrJ >= LSDPTFLL (rj)) 4 ] «ал^ч' WM fr'^COP Готовимся к вычислению q . Умножаем три старших разряда части (а|а|_1...а;_п)в делимого на d и присваиваем полученные значения переменным ri, ri 1 и ri_2. Отдельно рассматриваем случай, когда указанная часть делимого состоит ровно из трех разрядов. I При первом проходе цикла имеем как минимум три разряда: в предположении, что сам делитель b состоит как минимум из двух разрядов, существуют старшие разряды am+n_| и am+n_2 делимого, а разряд am+n мы положили равным нулю при инициализации вектора г_1. ri = (USHORT)((*msdptrrJ « d) + (*(msdptrr_l - 1) » sbitsminusd)); ri_1 = (USHORT)((*(msdptrr_l - 1) « d) + (*(msdptrr_l - 2) » sbitsminusd)); if (msdptrrj - 3 > rj) /* Четыре разряда делимого */ { ri_2 = (USHORT)((*(msdptrr_l - 2) « d) + (*(msdptrr_l - 3) » sbitsminusd)); } else /* Только три разряда делимого */ { ri_2 = (USHORT)(*(msdptrrJ - 2) « d); Теперь дело дошло до вычисления аппроксимации q, которой соответствует переменная qhat. Будем различать случаи ri Ф Ьп (частый) и ri = bn (редкий).
Криптография на Си и C++ в действии т ^ if (ri != bn) /* Почти всегда */ { qhat = (USHORT)((rhat = ((ULONG)ri « BITPERDGT) + (ULONG)ri_1)/bn); right = ((rhat = (rhat - (ULONG)bn * qhat)) « BITPERDGT) + ri_2; Неравенство bn_1 * qhat > right означает, что qhat превышает истинное значение минимум на 1 и максимум на 2. if ((left = (ULONG)bn_1 * qhat) > right) { qhat--; Уменьшив qhat на 1, повторяем проверку только тогда, когда rhat = rhat + bn < BASE (в противном случае и так выполняется неравенство bn_1 * qhat < BASE2 < rhat * BASE). if ((rhat + bn) < BASE) { if ((left - bn_1) > (right + ((ULONG)bn « BITPERDGT))) { qhat--; } } } } else Во втором, более редком случае ri = bn сначала полагаем значение q равным BASE - 1 = 216 - 1 = BASEMINONE. Тогда для rhat получаем: rhat = ri * BASE + ri_1 - qhat * bn = ri_1 + bn. Если rhat < BASE, то проверяем, не слишком ли велико значение qhat. В противном случае и так выполняется неравенство bn_1 * qhat < BASE2 < rhat * BASE. Проверку повторяем при том же условии, что и выше. { qhat = BASEMINONE;
ГДАвА 4- Основные операции right = ((ULONG)(rhat = (ULONG)bn + (ULONG)ri_1) « BITPERDGT) + h_2; if (rhat < BASE) { if ((left = (ULONG)brM * qhat) > right) { qhat--; if ((rhat + bn) < BASE) { if ((left - bn_1) > (right + ((ULONG)bn « BITPERDGT))) { qhat-; } } I Вычитаем произведение qhat • b из части и := (ajaj_|... aj_n)B делимого, которая заменяется полученной разностью. Продолжаем умножение и вычитание, сдвигаясь каждый раз на один разряд. Здесь нужно помнить вот о чем. Произведение qhat • bj может быть и двухразрядным. Оба разряда до поры до времени хранятся в переменной carry типа ULONG. Старший разряд переменной carry рассматривается как перенос при вычитании следующего по старшинству разряда. В том случае, когда разность u - qhat • b отрицательна (то есть значение qhat больше истинного на 1), следует вычислить значение u' := Bn+1 +и - qhat • b и рассматривать результат по модулю Bn+1 как В-дополнение и значения и. После вычитания старший разряд u'i+i числа и' записываем в старшее слово переменной borrow типа ULONG. Неравенство u'i+i ^0 в точности означает, что значение qhat слишком велико. В этом случае исправляем результат, вычисляя сумму ik-u' + Ь по модулю Bn+1. Случай корректировки рассмотрим несколько позже. borrow = BASE; carry = 0;
74 Криптография на Си и C++ в действии for (bptrj = LSDPTRJ. (b_l), rptrj = IsdptrrJ; bptrj <= msdptrbj; bptr_l++, rptr_l++) { if (borrow >= BASE) { *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASE - (ULONG)(USHORT)(carry = (ULONG)*bptr_l * qhat + (ULONG)(USHORT)(carry » BITPERDGT)))); } else { *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASEMINONEL- (ULONG)(USHORT)(carry = (ULONG)*bptr_l * qhat + | (ULONG)(USHORT)(carry » BITPERDGT)))); ]| } ' "| } I if (borrow >= BASE) \ { *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASE - (ULONG)(USHORT)(carry » BITPERDGT))); } else { *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASEMINONEL - (ULONG)(USHORT)(carry » BITPERDGT))); Запоминаем разряд частного на случай если понадобится корректировка. *qptr_l = qhat;
ГЛАВА 4. Основные операции 75 Как и было обешано, проверяем, не превышает ли разряд частного истинное значение на 1. Этот случай встречается чрезвычайно редко (ниже мы введем для него специальную проверку) и проявляется в том, что старшее слово переменной borrow типа ULONG равно нулю, то есть borrow < BASE. Тогда нужно положить u <— u' + b по модулю Bn+1 (см. выше). if (borrow < BASE) { carry = 0; for (bptrj = LSDPTR_L (bj), rptrj = IsdptrrJ; bptrj <= msdptrbj; bptr_l++, rptrj++) { *rptrj = (USHORT)(carry = ((ULONG)*rptrJ + (ULONG) (*bptrj) + (ULONG)(USHORT)(carry » BITPERDGT))); } *rptrj += (USHORT)(carry » BITPERDGT); (*qptrj)~; } f j 1 Устанавливаем указатели на остаток и частное и возврашаемся к началу основного цикла. msdptrrj--; IsdptrrJ-; qptrj--; } Определяем длины частного и остатка. Число разрядов частного может не больше чем на 1 превышать разность между числом разрядов делимого и делителя. Алина остатка не может превышать длины делителя. В обоих случаях определяем истинную длину, отбрасывая все ведущие нули.
76 Криптография на Си и C++ в действии SETDIGITSJ. (quotj, DIGITS_L (rj) - DIGITS_L (bj) + 1); RMLDZRS_L (quotj); SETDIGITSJ- (rj, DIGITS_L (bj)); cpyj (remj, rj); return E_CLINT_OK; В случае «короткого деления» делитель состоит всего из одного разряда Ь0, на который делится двухразрядное число (га;)в, где а; пробегает все разряды делимого. Устанавливаем начальное значение остатка: г <— О, а затем полагаем равным разности г <— (ra;)B - qb0. Значение г представлено переменной rv типа USHORT, значение (га;)в - переменной rhat типа ULONG. shortdiv: rv = 0; bv = *LSDPTRJ_ (bj); for (rptrj = MSDPTR_L (rj), qptrj = quotj + DIGITS J. (rj); rptrj >= LSDPTR_L (rj); rptrj--, qptrj--) *qptrj = (USHORT)((rhat = ((((ULONG)rv) « BITPERDGT) + (ULONG)*rptrJ)) / bv); rv = (USHORT)(rhat - (ULONG)bv * (ULONG)*qptrJ); } SETDIGITS_L (quotj, DIGITS_L (rj)); RMLDZRS_L (quotj); u2clintj (remj, rv); return E_CLINT_OK; } Время t = O(tnn) работы функции деления то же, что и для умножения, где тип- числа разрядов соответственно делимого и делителя в системе счисления с основанием В.
ГЛАВА 4. Основные операции 77 Теперь мы хотим представить на суд читателя несколько разновидностей алгоритмов деления с остатком, основанных на только что рассмотренной универсальной функции. Прежде всего, введем смешанную версию деления, когда делимое имеет тип CLINT, a делитель - тип USHORT. Для этого обратимся к подпрограмме для делителей малой длины функции div_l(). В ней практически ничего менять не надо, поэтому приведем только интерфейс. функция: Синтаксис: Вход: Выход: Возврат: Деление переменной типа CLINT на переменную типа USHORT int udivj (CLINT dvj, USHORT uds, CLINT qJ.CLINT rj); dvj (делимое), uds (делитель) qj (частное), rj (остаток) E_CLINT__OK, если все в порядке E_CLINT_DBZ в случае деления на 0 Как уже отмечалось, в ряде случаев вычислять частное не нужно, а интересен только остаток. Это не дает большой экономии времени, но, по крайней мере, таскать указатель к ячейке, где хранится частное, не имеет смысла. Тогда логично было бы написать самостоятельную функцию для вычисления остатков, или «вычетов». Математическую подоплеку использования этой функции нам предстоит подробно изучить в главе 5. Функция: Синтаксис: Вход: Выход: Результат: Вычисление остатка (вычета по модулю п) int rnodj (CLINT dj, CLINT п_1, CLINT rj); dj (делимое), nj (делитель или модуль) rj (остаток) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 Остаток вычисляется значительно проще, если модуль равен степени двойки, а именно 2к, так что и для этого случая сгодится своя собственная функция. Остаток делимого от деления на 2 получается отбрасыванием всех двоичных разрядов после к-го, при этом отсчет начинается с 0. Такое отбрасывание соответствует побитной логической операции AND (см. п. 7.2) делимого и числа 2*-1= (П1111...1)2, состоящего из к двоичных единиц. Самым важным объектом при выполнении этой операция является тот разряд делимого, представленного в системе счисления с основанием В, который содержит к-и бит. Все более старшие разряды делимого нас не интересуют. Делитель в соответствующей функции mod_l() представлен только показателем к.
78 •if Криптография на Си и C++ в действии I функция: Вычисление остатка от деления на степень двойки (вычисление вычета по модулю 2к) | Синтаксис: int mod2_l (CLINT d_l, ULONG k, CLINT rj); | Вход: dj (делимое), к (показатель степени делителя или модуля) I Возврат: г_1 (остаток) int mod2_l (CLINT dJ. ULONG k, CLINT rj) { int i; Поскольку 2 > 0, проверять случай деления на 0 не нужно. Сначала копируем значение dj в rj. Если к превышает максимальную двоичную длину, допускаемую типом CLINT, то завершаем процедуру. Hit г. , cpyj (rj, dj); if (k > CLINTMAXBIT) return E_CLINT_OK; Определяем тот разряд переменной г_1, в котором нужно что-то менять, и присваиваем его номер переменной i. Если значение i превышает число разрядов в rj, то завершаем процедуру. i = 1 + (k » LDBITPERDGT); if (i > DIGITS_L (rj)) return E_CLINT_OK; Теперь применяем логическую операцию AND к разряду переменной rj (отсчитываем с 1), определенному на предыдущем шаге, и значению 2kmodB,TPERDCT- 1 (= 2kmod16- 1 в нашей реализации). Новое значение i числа разрядов переменной rj запоминаем в r_l[0]. Удаляем нулевые старшие разряды и получаем результат. U[i] &= (1U « (к & (BITPERDGT - 1))) - 1U; SETDIGITSJ. (rj, i); RMLDZRS_L (rj);
ГЛАВА 4. Основные операции 79 return E_CLINT_OK; } В смешанном варианте функции вычисления вычетов делитель имеет тип USHORT, остаток тоже представляется типом USHORT. Здесь мы опять приводим только интерфейс; сами функции читатель сможет найти в исходных текстах пакета FLINT/C. функция: Вычисление остатка, деление переменной типа CLINT на переменную типа USHORT Синтаксис: USHORT umodj (CLINT dv_l, USHORT uds); Вход: dvj (делимое), uds (делитель) Возврат: > неотрицательный остаток, если все в порядке OxFFFF в случае деления на О При тестировании программ, реализующих деление, - да и любых других программ вообще, - следует учесть некоторые моменты (см. главу 12). В частности, шаг 5 нужно проверять подробно, поскольку для случайно выбранных контрольных значений он выполняется лишь с вероятностью 21В (= 2~15 в нашем случае) (см. [Knut], п. 4.3.1, Упражнение 21). Следующие контрольные значения делимого а и делителя Ъ (с уже вычисленными частным q и остатком г) подобраны так, что та часть программы, которая реализует шаг 5, выполняется дважды. Еще несколько таких контрольных значений читатель найдет в тестовой программе testdiv.c. Контрольные значения даны в шестнадцатиричном виде, разряды идут справа налево в порядке возрастания, длина не указана. Контрольные значения для шага 5 алгоритма деления а = еЗ 7d За be 90 4b ab а7 а2 ас 4b 6d 8f 78 2b 2b f8 49 19 d2 91 73 47 69 Od 9e 93 dc dd 2b 91 ce e9 98 3c 56 4c f1 31 22 06 c9 1e 74 d8 Ob a4 79 06 4c 8f 42 bd 70 aa aa 68 9f 80 d4 35 af c9 97 ce 85 3b 46 57 03 c8 ed ca b = 08 0b 09 87 b7 2c 16 67 c3 0c 91 56 a6 67 4c 2e 73 e6 1a 1f d5 27 d4 e7 8b 3f 15 05 60 3c 56 66 58 45 9b 83 cc fd 58 7b a9 b5 fc bd cO ad 09 15 2e 0a c2 65 q = 1c 48 a1 c7 98 54 1a eO b9 eb 2c 63 27 M ft ff f4 fe 5c 0e27 23 r = ca 23 12 fb b3 f4 c2 3a dd 76 55 e9 4c 34 10 M 5c 60 64 bd 48 a4 e5 fc c3 3d df 55 3e 7c b8 29 bf 66 fb fd 61 b4 66 7f 5e d6 b3 87 ec 47 c5 27 2c f6 fb
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов А вы можете исчезать и появляться не так внезапно? А то у меня голова идет кругом. Хорошо, - сказал Кот и исчез - на этот раз очень медленно. Первым исчез кончик его хвоста, а последней - улыбка; она долго парила в воздухе, когда все остальное уже пропало. Д-да! - подумала Алиса. - Видала я котов без улыбок, но улыбка без кота! Льюис Кэррол, Приключения Алисы в Стране Чудес Начнем эту главу с основных правил деления с остатком. Попытаемся объяснить важность деления с остатком, его возможные приложения и способы вычисления. Ну а для начала - немного алгебры, чтобы читатель смог понять те функции, которые мы введем позже. Мы уже знаем, что при делении с остатком целого числа а е Z на натуральное число 0 < т е IN существует единственное представление а = qm + г, О < г < т. Число г называется остатком от деления а на т, или вычетом по модулю т. При этом число т делит разность а - г, обозначается как т | {а - г). К. Гаусс ввел для этого соотношения другое обозначение: а = г mod т (читается «а сравнимо с г по модулю /и»). Сравнимость по модулю натурального числа т является отношением эквивалентности на множестве целых чисел. Это означает, что множество R := {(a, b)\ a = b mod m} пар целых чисел таких, что т | (а - Ь), обладает следующими свойствами: (a) Rрефлексивно: для любого целого числа а пара (а, а) лежит в R; то есть а = a mod т. ъъя Карл Фридрих Гаусс (1777-1855) - один из величайших математиков всех времен. Сделал множество важных открытий в математике и естественных науках. В возрасте 24 лет опубликовал знаменитую работу Disqiiisitiones Arithmeticae, послужившую основой для современной теории чисел.
82 Криптография на Си и C++ в действии (б) R симметрично: если (а, Ь) е /?, то (&, а) е R\ то есть из я = Ъ mod /и следует, что b = a mod /и. (в) R траизитивно: если (а, й) е R, (Z?, с) е Я, то (а, с) е R\ то есть из а = b mod mub = c mod ш, следует, что а = с mod ш. Доказательство этих свойств следует непосредственно из опреде- '.'•;* ления операции деления с остатком. Отношение эквивалентности R делит множество целых чисел на непересекающиеся подмножества, называемые классами эквивалентности: для данного остатка г и натурального числа т > О множество г := {а | а = г mod ш}, ♦*•.'»'' W- н ; 1 или, в других обозначениях, r + тЖ, называется классом вычетов , чмсла г «о модулю т. Элементами этого класса являются все целые] числа, дающие при делении на т один и тот же остаток г. 'W*J * - Например, пусть ш = 7, г =5; тогда множество целых чисел, даю-J щих при делении на 7 остаток 5, - это класс вычетов 5 = 5+ 7-Z= {...,-9,-2,5, 12, 19,26,33, ...}. Два класса вычетов по модулю фиксированного числа т либо совпадают, либо не пересекаются. Следовательно, класс вычетов однозначно определяется любым из своих элементов. Элементы класса вычетов называются представителями', любой элемент может служить представителем класса. Равенство классов вычетов эквивалентно сравнимости представителей этих классов по данному модулю. Поскольку при делении с остатком остаток всегда меньше делителя, для любого целого т существует конечное число классов вычетов по модулю т. • з:* кЖУ:, Теперь мы, наконец, вплотную приблизились к сути наших рассуждений. Классы вычетов - это такие объекты, в которых можно выполнять арифметические действия, оперируя лишь с представи- ] л? телями. Вычисления в классах вычетов играют огромную роль в алгебре и теории чисел, а значит, незаменимы в теории кодирования и современной криптографии. Далее мы попытаемся пояснить алгебраические аспекты модульной арифметики. Пусть а, Ъ Vim- целые числа, т > 0. Для классов вычетов аи bnol модулю т определим операции «+» и «•», которые назовем сложени-1 ем и умножением (классов вычетов), поскольку определяются они noj аналогии с соответствующими операциями над целыми числами: а + Ь:= а + b (сумма классов равна классу суммы); a-b\-a-b (произведение классов равно классу произведения). Множества называются непересекающимися, если у них нет общих элементов или, иначе, если их пересечение есть пустое множество.
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов 83 Обе операции определены корректно, поскольку в обоих случаях результат является классом вычетов по модулю т. Множество Жт := { г | г - вычет по модулю т} классов вычетов по модулю ш, на котором определены эти две операции, называется конечным коммутативным кольцом (Жт, +, •) с единицей, что, в частности, 1 '<*! подразумевает выполнение следующих аксиом: м (а) Замкнутость по сложению: Сумма двух элементов множества Жт является элементом множества Жт. (б) Ассоциативность сложения: Для любых я, Ь, с из Жт справедливо а + (Ь + с) = (а + Ь) + с . (в) Существование нулевого элемента: Для любого а из Жт справедливо а + 0 = а . (г) Существование противоположного элемента: Для любого а из Жт существует единственный элемент Ъ из Жт такой, что а + Ъ - 0 . (д) Коммутативность сложения: Для любых а, Ъ из Жт справедливо а + b = Ъ + а . (е) Замкнутость по умножению: Произведение двух элементов множества Жт является элементов множества Жт. (ж) Ассоциативность умножения: Для любых а, Ъ, с из Жт справедливо a-(b-c) = (a-b)-c. (з) Существование единичного элемента: Для любого а из Жт справедливо а • 1 = а . (и) Коммутативность умножения: Для любых a, b из Жт справедливо a-b-b-a, (к) В кольце (Zw, +, •) выполняется закон дистрибутивности: а-фл-с^-а-Ъл-а-с. На основании свойств 1-5 можно заключить, что множество (Zm, +) является абелевой группой, где термин «абелева» означает коммутативность сложения. Свойство 4 позволяет определить на множестве Жт операцию вычитания как сложение с противоположным элементом: если элемент с является противоположным к Ь, то b + с = 0, а значит, для любого а е Жт a— b := а + с.
84 Криптография на Си и C++ в действии' На множестве (Zm, •) справедливы групповые законы 6-9 для операции умножения, единицей является элемент 1. Однако не для каждого элемента множества Жт обязательно существует обратный, то есть (Z„„ •), вообще говоря, является не группой, а лишь коммутативной полугруппой с единицей.3 Но если исключить из Жт все элементы, не взаимно простые с т (в том числе и 0), то полученная структура будет абелевой группой по умножению (см. п. 10.2). Обозначим ее через (Zw , •)• Значимость алгебраических структур, аналогичных группе (Жт , •), можно пояснить на примере некоторых хорошо известных коммутативных колец. Множество Z целых чисел, множество Q рациональных чисел и множество R вещественных чисел - все это коммутативные кольца с единицей, которые, в отличие от (Zw , •), бесконечны (на самом деле, множество вещественных чисел является полем, то есть обладает некоторыми дополнительными свойствами). Все арифметические правила, приведенные выше для конечного кольца, хорошо нам известны - ведь мы пользуемся ими каждый -вт , ":, день. В главе 12 они будут нам верными помощниками, когда при-1 дет время тестировать арифметические функции. А пока соберем о I них важную информацию. I При вычислении в классах вычетов мы оперируем исключительно с представителями этих классов. Из каждого класса вычетов по модулю т выбираем ровно по одному представителю и получаем таким s .„...-. . образом полную систему вычетов, в рамках которой и будем проводить все вычисления. Система наименьших неотрицательных вычетов по модулю т представляет собой множество Rm := {0, 1, ..., ш-1}. Множество чисел г, удовлетворяющих неравенству -ут<г<уш, будем называть системой абсолютно наименьших вычетов по модулю т. В качестве примера рассмотрим кольцо Ж2в = { 0,1,..., 25 }. Системой наименьших неотрицательных вычетов по модулю 26 будет множество R2e={0, 1, ..., 25}, системой абсолютно наименьших вычетов - множество {-12, -11, ..., 0, 1, ..., 13}. Поясним связь между арифметикой в классах вычетов и модульной арифметикой в системах вычетов: равенство 18 + 24 = 18 + 24 = 16 эквивалентно сравнению 18+ 24 = 42 = 16 mod 26; Множество (Н, *) является полугруппой, если на множестве Н определена ассоциативная операция *.
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов 85 равенство 9-15 = 9 + 11 = 20 эквивалентно сравнению 9-15 = 9+11 =20 mod 26. Сопоставляя английскому алфавиту кольцо классов вычетов Z26 или множеству ASCII-символов кольцо Z256> можно производить вычисления с символами. Юлию Цезарю приписывают простейшую систему шифрования, в которой каждая буква открытого текста складывается по модулю 26 с некоторым фиксированным элементом кольца Ж2в (у Цезаря это был элемент 3). Таким образом, каждая буква алфавита сдвигалась на три позиции вправо, при этом X переходил bA,YbBhZbC.4 Для вычислений в классах вычетов можно составить таблицы сложения и умножения. Покажем, как это делать на примере кольца Z5 (таблицы 5.1 и 5.2 соответственно). Таблица 5.1. Таблииа сложения по молулю 5 + 0 1 2 3 4 0 0 1 2 3 4 1 1 2 3 4 0 2 2 3 4 0 1 3 3 4 0 1 2 4 4 0 1 2 3 Таблииа 5.2. Таблииа умножения по молулю 5 1 2 3 4 1 1 2 3 4 2 1 4 1 3 3 3 1 4 2 4 4 3 2 1 То, что множество классов вычетов является конечным, дает нам значительные преимущества по сравнению с такими бесконечными структурами, как кольцо целых чисел, поскольку при выполнении арифметических операций в компьютерной программе у нас никогда не возникнет переполнения, если только будут выбраны подходящие представители классов вычетов. Операция обработки результата, например функцией rnodJQ, называется приведением СМ. Aulus Gellius, XII, 9 и Suetonius, Caes. LVI.
86 Криптография на Си и C++ в действии (по модулю ш). Теперь мы можем вычислять сколько душе угодно, ограничив лишь представление чисел и функций пакета FLINTVC полной системой вычетов по модулю т, где т < /Vmax. Будем всегда оперировать с положительными представителями и работать в рамках системы неотрицательных вычетов. Свойства классов вычетов позволяют нам работать в пакете FLINT/C с большими числами ти- iWWsM na CLINT. Трудности могут возникать лишь в отдельных ситуациях, которые мы специально обсудим. Довольно теории об арифметике в классах вычетов. Займемся теперь функциями, реализующими модульную арифметику. Сначала вспомним функции modJO и mod2J() из п. 4.3, вычислявшие остаток от деления на т и 2к соответственно, а затем перейдем к функ- 1 > циям модульного сложения, вычитания, умножения и возведения в квадрат. Модульному возведению в степень, как особенно сложной теме, будет посвящена отдельная глава. * " - Для удобства в обозначении класса вычетов будем опускать черту и | вместо а писать а. Принцип работы функций модульной арифметики заключается в1 следующем: к операндам применяется соответствующая обычная («немодульная») функция, а затем результат делим с остатком на модуль. Следует, однако, отметить, что размер промежуточных результатов может достигать 2MAXfi разрядов, а в случае вычитания могут появляться отрицательные числа, что недопустимо в типе CLINT. Ранее мы назвали такие ситуации переполнением и потерей значащих разрядов соответственно. В основных арифметических функциях предусмотрен механизм обработки этих ситуаций: промежуточные результаты рассматриваются как вычеты по модулю ОЧшх + 1) (см. главы 3 и 4). Этот же метод можно применять и сейчас, в случаях, когда конечный результат модульной операции имеет тип CLINT. Чтобы получить верный результат в случаях переполнения и потери значащих разрядов, позаимствуем из уже рассмотренных нами в главе 4 функций базовые функции void add (CLINT, CLINT, CLINT); void sub (CLINT, CLINT, CLINT); void mult (CLINT, CLINT, CLINT); t! r "n void umul (CLINT, USHORT, CLINT); void sqr (CLINT, CLINT); |l Эти функции, выделенные из функций add_l(), sub_l(), mulJO ^ sqrJ(), с которыми мы работали раньше, служат для выполнения собственно арифметических операций. Остальные операции: удаление старших нулевых разрядов, заполнение сумматора и обработка возможного переполнения или потери значащих разрядов - все, что осталось на долю ранее введенных функций. Их синтаксис,
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов 87 и семантика не изменяются, то есть мы по-прежнему можем ими пользоваться. Рассмотрим это преобразование на примере функции умножения mul_l() (сравните с реализацией этой же функции на стр. 49). функция: Синтаксис: Вход: Выход: Возврат: Умножение int mulj (CLINT f1_l, CLINT f2_l, CLINT ppj); f1 J, f2J (сомножители) ppj (произведение) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int mulj (CLINT NJ, CLINT f2_l, CLINT ppj) { CLINT aaj, bbj; CLINTD pj; Int OFL = 0; r Удаление велуших нулей и заполнение сумматора. :'1 Г cpy_l (aa_l, П_1); cpy_l (bb_l, f2_l); Вызов базовой функции умножения. О mult (aa_l, bbj, pj); if (DIGITS J_ (pj) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? 7 {
88 Криптография на Си и C++ в действии 'ЭЖ'. »*л ANDMAX_L (p_l); /* Привести по модулю (Nmax +1)7 OFL = E_CLINT_OFL; } еру J (ppj, p_l); return OFL; Аналогично изменяем и остальные функции: add_l(), sub_l() и sqr_l(). Сами по себе базовые арифметические функции не содержат новых компонентов и поэтому здесь не приводятся; подробнее см. реализацию на flint.c. Базовые функции не вызывают переполнения, поэтому приведение по модулю (7Vmax + 1) в них не выполняется. Они являются внутренними компонентами функций пакета FLINT/C и поэтому имеют описатель static. Работая с базовыми функциями, следует помнить, что они не могут оперировать с числами с ведущими нулями и что их нельзя использовать в режиме сумматора (см. главу 3). Функция sub() предполагает, что разность положительна. В противном случае результат не определен, поскольку такая ситуация функцией sub() не предусмотрена. И наконец, при вызове базовых функций следует выделить пространство под промежуточные результаты большой длины. В частности, для представления результата функции sub() требуется по крайней мере столько же памяти, сколько для представления уменьшаемого. Что ж, теперь у нас есть все, чтобы разработать основные функции модульной арифметики: madd_l(), msub_l(), mmul_l() и msqr_l(). Функция: Синтаксис: Вход: Выход: Возврат: Модульное сложение int maddj (CLINT aa_l, CLINT bb_l, CLINT c_ aaj, bbj (слагаемые), mj (модуль) cj (остаток) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 , CLINT mj); int maddj (CLINT aaj, CLINT bbj, CLINT с J, CLINT mj) { CLINT a J, bj; dint tmp_l[CLINTMAXSHORT + 1];
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов if (EQZ_L (m_l)) { return E_CLINT_DBZ; } cpyj (aj, aaj); v*< r cpyj (b_l, bbj); чг. if (GE_L (aJ, mj) || GE_L (bj, m_l)) { "^ add (aJ, bj, tmpj); modj (tmpj, mj, cj); } else Если обе переменных а_1 и bj меньше модуля m_l, то делить с остатком не нужно. { add (aj, bj, tmpj); if (GE_L (tmpj, mj)) { subj (tmpj, mj, tmpj); /* Исключаем потерю значащих разрядов */ } В предыдущем вызове функции subJO мы немного подстраховались, введя переменную tmpj. Эта переменная, где лежит сумма переменных a J и bj, может лишь на один разряд превышать константу МАХВ. Внутри функции subJO никаких нарушений быть не должно, поскольку память для хранения дополнительного разряда мы выделили. Таким образом, результат мы записываем в tmpj, а не сразу в cj, как можно было бы ожидать. Зато после выполнения функции subJO переменная tmpj у нас имеет не больше МАХВ разрядов.
90 Криптография на Си и C++ в действии cpyj (c_l, tmpj); } return E_CLINT_OK; } Функция модульного вычитания msub_l() оперирует только с неотрицательными промежуточными результатами функций add_l(), sub_l() и mod_l(), то есть мы не выходим за рамки системы наименьших неотрицательных вычетов. I Функция: Модульное вычитание | Синтаксис: int msubj (CLINT aa_l, CLINT bb_l, CLINT cj, CLINT mj); | Вход: aaj (уменьшаемое), bbj (вычитаемое), mj (модуль) | Выход: cj (остаток) | Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на О int msubj (CLINT aaj, CLINT bbj, CLINT с J, CLINT mj) { CLINT aj, bj, tmpj; If (EQZ_L (m J)) return E_CLINT_DBZ; } cpyj (aj, aaj); cpyj (bj, bbj); Будем различать случаи aj > bj и а_1 < b_l. В первом случае поступаем как обычно; во втором случае вычисляем разность (bj - aj), приводим ее по модулю mJ и вычитаем полученное положительное число из m I. if (GE_L (aj, bj)) /* aj - b J > 0 7 {
5. Модульная арифметика: вычисление в классах вычетов 91 sub (a_l, bj, trnpj); modj (tmpj, mj, cj); } else /* a J - bj < 0 7 sub (bj, a_l, tmpj); modj (tmpj, mj, tmpj); if (GTZ_L (tmpj)) { sub (mj, tmpj, cj); } else { SETZERCLL (cj); } } return E_CLINT_OK; } Теперь перейдем к функциям mmulj() и msqrj() модульного умножения и возведения в квадрат. Функция: Модульное умножение Синтаксис: int mmulj (CLINT aaj, CLINT bbj, CLINT cj, CLINT mj); Вход: aaj, bbj (сомножители), mj (модуль) Выход: с J (остаток) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на О int mmulj (CLINT aaj, CLINT bbj, CLINT cj, CLINT mj) { CLINT aj, bj; CLINTD tmpj; if (EQZ_L (mJ)) {
92 Криптография на Си и C++ в действии return E_CLINT_DBZ; cpy_l (aj, aa_l); cpyj (b_l, bbj); mult (a_l, bj, tmpj); modj (tmpj, m_l, cj); return E_CLINT_OK; } Функция модульного возведения в квадрат строится аналогично, поэтому для нее приведем только интерфейс. Функция: Синтаксис: Вход: Выход: Возврат: Модульное возведение в квадрат int msqr_l(CLINT aaj, CLINT с J, CLINT mj); aaj (множитель), mj (модуль) cj (остаток) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 Для каждой из этих функций (разумеется, за исключением возведения в квадрат) можно определить соответствующую смешанную функцию, у которой второй аргумент имеет тип USHORT. Покажем, как это делается, на примере функции umaddj(). Функции umsubJO и ummulJO строятся по образу и подобию, так что их приводить не будем. Функция: Модульное сложение переменных типа CLINT и USHORT Синтаксис: int umaddj (CLINT a_l, USHORT b, CLINT cj, CLINT mj); Вход: a J, b (слагаемые), mj (модуль) Выход: cj (остаток) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на О int umaddj (CLINT a_l, USHORT b, CLINT с J, CLINT mj) { int err; CLINT tmpj;
f ДАВА 5. Модульная арифметика: вычисление в классах вычетов 93 -А- u2clint_l (trnpj, b); err = maddj (aj, trnpj, cj, mj); return err; } В следующей главе мы пополним нашу коллекцию смешанных функций с аргументом типа USHORT еще двумя функциями. А заканчивая эту главу, мы бы хотели, используя модульное вычитание, построить еще одну полезную вспомогательную функцию, которая определяла бы, являются ли две переменные типа CLINT представителями одного и того же класса вычетов по модулю т. В основу функции mequ_l() положено определение отношения сравнимости: а = b mod т <=> т \ {а - Ь). Чтобы выяснить, сравнимы ли два CLINT-объекта по модулю mj, нам нужно всего лишь применить функцию msubj(a_l, b_l, r_l, mj) и проверить, равен ли нулю полученный остаток rj. I Функция: Проверка сравнимости по модулю т I Синтаксис: int mequj (CLINT aj, CLINT bj, CLINT m_ | Вход: aj, bj (операнды), mj (модуль) | Возврат: 1, если (aj == bj) по модулю mj О в противном случае int mequj (CLINT a J, CLINT bj, CLINT mj) { CLINT rj; if (EQZ_L (mj)) { return E_CLINT_DBZ; msubj (aj, bj, rj, mj); return ((0 == DIGITSJ. (r_l))?1:0); }
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень - Скажите, пожалуйста, куда мне отсюда идти? - А куда ты хочешь попасть? - ответил Кот. - Мне все равно... - сказала Алиса. - Тогда все равно, куда и идти, - заметил Кот. - ...только бы попасть куда-нибудь, - пояснила Алиса. - Куда-нибудь ты обязательно попадешь, - сказал Кот. - Нужно только достаточно долго идти. Льюис Кэррол, Приключения Алисы в Стране Чудес, (Перевод с английского Н. Демуровой) В дополнение к правилам вычисления суммы, разности и произведения в классах вычетов определим операцию возведения в степень, где показатель указывает, сколько раз основание умножается само на себя. Как правило, возведение в степень реализуется рекурсивным вызовом операции умножения: для а из кольца Zw справедливо а0 := 1 и ae+l := a • ае. Легко видеть, что для операции возведения в степень в Zw выполняются обычные правила (см. главу 1): ае • J = ae+f, ae-be = (a- b)\ (ae)f = aef. 6.1. Первые шаги Самый простой способ модульного возведения в степень - рекурсивно применять указанное выше правило, умножая основание а само на себя е раз. Для этого требуется е - 1 модульных умножений, а это для наших целей уж слишком много. Более эффективный способ иллюстрируется следующими примерами, в которых рассматривается двоичное представление показателя: 15 2-Ч2-+2+1 а = а 4ш*)* *«-**-{№)
96 Криптография на Си и C++ в действии ..ч &Ш'< "i jP!,)CI '.НИ щшшж*ш^ V Здесь для возведения основания в 15-ю степень требуется всего шесть умножений, тогда как в первом способе нам потребовалось бы 14 умножений. Половина из них - это возведение в квадрат, для которого, как мы знаем, нужно примерно вдвое меньше машинной^ времени по сравнению с обычным умножением. Для возведения щ 16-ю степень требуется всего 4 возведения в квадрат. Я Как мы увидим, алгоритмы вычисления экспоненты ае по модулкя т, использующие двоичное представление показателя, как правило,! намного более предпочтительны, чем первый подход. Но прежде следует отметить, что промежуточные результаты многократного целочисленного умножения быстро занимают столько памяти, что их не в состоянии хранить ни один компьютер в мире, поскольку из р = аъ следует log p = b log а, таким образом, число разрядов экспоненты аь есть произведение показателя на число разрядов основания. Однако эту проблему можно решить, если проводить вычисления в кольце классов вычетов Жт посредством модульного умножения. Фактически в большинстве прикладных задач требуется возведение в степень именно по модулю т, так что эту ситуацию и будем рассматривать. Пусть е = (еп-\еп_2.-'ео)г> где e#,-i > 0, - двоичное представление показателя е. Тогда следующий бинарный алгоритм требует Llog2eJ = п модульных возведений в квадрат и 8(e) - 1 модульных умножений, где i=0 есть число единиц в двоичном представлении показателя е. Если считать, что каждый разряд принимает значение 0 или 1 равновероятно, то можно сказать, что среднее значение 8(e) = nil и алгоритм требует всего -|-[_log2 e\ умножений. Бинарный алгоритм вычисления ае по модулю т 1. Вычислить р<г-ае"~1 и / <— п-2. 2. Положить р <r- p mod т. 3. Если et= 1, то положить р <с-р • a mod т. 4. Положить i<— i - 1; при / > 0 вернуться на шаг 2. 5. Результат:/?. Следующая функция, реализующая этот алгоритм, дает хорошие результаты уже для малых показателей степени, представимых типом USHORT.
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 97 Смешанное модульное возведение в степень с показателем типа USHORT int umexpj (CLINT basj, USHORT e, CLINT pj, CLINT mj); basj (основание) e (показатель) m_l (модуль) pj (вычет по модулю m_l) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на О int umexpj (CLINT basj, USHORT e, CLINT pj, CLINT mj) { CLINT tmpj, tmpbasj; USHORT k = BASEDIV2; interr=E_CLINT_OK; if (EQZ_L (m J)) { return E_CLINT_DBZ; /* Деление на нуль */ } if (EQONEJ. (mj)) { SETZERO_L (pj); /* Модуль = 1 ==> Остаток = 0 */ return E_CLINT_OK; } if (e == 0) /* Показатель = 0 ==> Остаток = 1 */ { SETONE_L (pj); return E_CLINT_OK; } if (EQZ_L (basj)) { SETZEROJ. (pj);
V 98 Криптография на Си и C++ в действии return E_CLINT_OK; } modj (basj, mj, tmpj); cpyj (tmpbasj, tmpj); После различных проверок определяем позицию старшего единичного разряда показателя е. Переменная к используется в качестве маски отдельных двоичных разрядов показателя е. Затем к сдвигается еше на одну позицию вправо, что соответствует операции i <— n - 2 на шаге 1 алгоритма. while ((е & к) == 0) { к»=1; } к »= 1; Аля остальных разрядов показателя е выполняем шаги 2 и 3. Маска к служит в качестве счетчика циклов и каждый раз сдвигается на один разряд вправо. Затем выполняется умножение на основание экспоненты по модулю m_l. while (k != 0) { msqrj (tmpj, tmpj, mj); if (e&k) { mmulj (tmpj, tmpbasj, tmpj, mj); } k»=1; } cpyj (p_l, tmpj); return err; } Преимущества бинарного алгоритма возведения в степень особенно видны, если основание степени мало. Для основания, имеющего i fry- ■■:Ч \ Щ. Щ
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 99 тип USHORT, все умножения р <— р • a mod т на шаге 3 бинарного алгоритма имеют тип CLINT * USHORT по модулю CLINT. Это дает существенное увеличение скорости по сравнению с другими алгоритмами, которые в этом случае потребовали бы умножения двух переменных типа CLINT. Конечно, возведения в квадрат (шаг 2) используют объекты типа CLINT, но здесь мы можем использовать более быструю функцию. Таким образом, попробуем реализовать функцию возведения в степень wmexpJO, парную к функции umexp_l() и применяемую для основания типа USHORT. Выделение по маске разрядов показателя степени - хорошее подготовительное упражнение с точки зрения последующих «больших» функций возведения в степень. По существу, мы последовательно сравниваем все разряды ev показателя с переменной Ь, первоначально имеющей 1 в старшем разряде, затем сдвигаем b вправо и повторяем процедуру до тех пор, пока b не станет равным 0. Для оснований и показателей длиной до 1000 бит функция wmexpJO работает примерно на 10% быстрее, чем универсальные функции, которыми мы займемся позже. Функция: Синтаксис: Вход: Выход: Возврат: Модульное возведение в степень основания типа USHORT int wmexpj (USHORT bas, CLINT ej, CLINT restj, CLINT mj); bas (основание), ej (показатель) mj (модуль) restj (вычет baseJ по модулю mj) E__CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 int wmexpj (USHORT bas, CLINT ej, CLINT restj, CLINT mj) { CLINT pj, zj; USHORT k, b, w; if (EQZ_L (m J)) return E_CLINT_DBZ; Г Деление на нуль */ }
100 Криптография на Си и C++ в действии ~$*>'Э if (EQONE_L (m_l)) { SETZERO_L (restj); return E_CLINT_OK; } if (EQZ_L (e_l)) /* Модуль = 1 ==> Остаток = 0 */ SETONEJ. (restj); return E_CLINT_OK; } if (0 == bas) { SETZERO.L (restj); return E CLINT OK; } SETONE_L (pj); cpyj (zj, ej); Разряды показателя zj обрабатываются, начиная со старшего ненулевого разряда в старшем слове показателя; при этом мы всегда выполняем сначала возведение в квадрат, а затем, если нужно, умножение. Проверка разрядов показателя осуществляется в выражении if ((w & b) > 0) путем их маскирования поразрядной операцией AND. b = 1 « ((IdJ (zj) - 1) & (BITPERDGT- 1UL)); w = zJ[DIGITS_L (zj)]; for(;b>0;b»=1) { msqrj (pj, pj, mj); if ((w & b) > 0) {
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 101 ummulj (p_l, bas, p_l, m_l); } } ] I Затем обрабатываются оставшиеся разряды показателя. for (k = DIGITS_L (zj) - 1; к > 0; к--) { w = zj[k]; for (b = BASEDIV2; b > 0; b »= 1) { msqrj (p_l, pj, mj); if ((w & b) > 0) { ummulj (pj, bas, pj, mj); } } } cpyj (rest_l, pj); return E_CLINT_OK; } 6.2. М-арное возведение в степень Обобщив бинарный алгоритм со стр. 96, можно еще уменьшить число модульных умножений при возведении в степень. Суть подхода состоит в том, чтобы записать показатель в системе счисления с основанием, большим 2, и заменить умножение на а на шаге 3 умножением на степени числа а. Итак, пусть показатель е представлен в системе счисления с основанием М: е = 0„-1е„-2.--<?о)л/, где число М мы определим позже. Для вычисления степеней a mod m используется следующий алгоритм.
102 Криптография на Си и C++ в действии (6.1) (6.2) М-арный алгоритм вычисления ае по модулю т 1. Вычислить и запомнить таблицу значений a2 mod in, a3 mod /и, ..., ам~1 mod пи 2. Положить р <— а'"'1 и i <— п - 2. 3. Положить р <r-pM mod т. 4. Если е( Ф 0, то положить р <— pa*7 modm . 5. Положить / <— / - 1; при / > 0 вернуться на шаг 3. 3 6. Результат: р. Понятно, что число умножений зависит от числа разрядов показателя е и, следовательно, от выбора основания системы счисления М. Поэтому определим число М так, чтобы на шаге 3 использовалось бы как можно больше возведений в квадрат, как это было в примере выше для 216. Тогда число умножений на степени числа а, вычисленные на шаге 1, будет минимальным, что оправдает затраты памяти на хранение таблицы. Исходя из первого условия, выбираем М равным степени двойки: М- 2к. Согласно второму условию, число модульных умножений рассматриваем как функцию от М: s j Потребуем, чтобы на шаге 3 выполнялось [logM e}og2 M = [_log2 e\ возведений в квадрат, а на шаге 4 в среднем \}°%м ejprfc, * 0) = log2e рг(е, Ф 0) *&***& > (63) модульных умножении, где 1 рг(*,*0) = 1- М есть вероятность того, что разряд et показателя е ненулевой. Учитывая, что для построения таблицы предвычислений нужно М - 2 умножений, получаем, что М-арный алгоритм требует в среднем \х{(к):=2к -2 + \\og2e\+ \og2e Н (6.4) = 2*-2 + Llog2eJ+ 1 + 2*-О к2к модульных возведений в квадрат и умножений.
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 103 Таблииа 6.1. Требования при воз велении в степень В таблице 6.1 приведены значения числа модульных умножений для вычисления ae mod m, когда показатель е и модуль m имеют длину 512 бит и М = 2к. Там же даны затраты памяти на хранение таблицы предвычислений степеней a mod ш, обусловленные вычислением произведения (2к - 2) * CLINTMAXSHORT * sizeof(USHORT). к 1 2 3 4 5 6 Число умножений 766 704 666 644 640 656 Память (в байтах) 0 1028 3084 7196 15420 31868 Как видно из таблицы, среднее число умножений достигает минимального значения 640 при к = 5, тогда как объем требуемой памяти увеличивается при каждом следующем к примерно вдвое. А как же будут меняться временные затраты для больших показа- u телей степени? На этот вопрос отвечает таблица 6.2, в которой приведено число модульных умножений, выполняемых при возведении в степень, при различных длинах показателя и различных М = 2к. В таблицу, 1 - помимо степеней двойки, включено значение 768, поскольку ключ такой длины часто используется в криптосистеме RSA (см. главу 16). aqju • Наименьшие значения для числа умножений выделены жирным шрифтом. При рассмотрении диапазонов чисел, для обработки которых был разработан пакет FL1NT/C, оказывается, что при к = 5 мы получаем универсальное основание М = 2к системы счисления. Но в этом случае нам потребуется довольно много памяти (15 кбайт) для хранения таблицы предвычислений а2, а3, ..., я31. Согласно работе [Cohe], п. 1.2, М-арный алгоритм можно улучшить, выполняя на этапе предвычислений не М - 2, а только М/2 умножений, то есть в два раза сократить затраты памяти. И теперь наша задача - вычислить ае mod m, где е = {еп_хеп.1...е^)м - представление показателя в системе счисления с основаниемМ-2.
104 Криптография на Си и C++ в действии Таблииа 6.2. Число умножений аля типичных ллин показателя и различных оснований 2к к 1 2 3 4 5 6 7 8 32 45 44 46 52 67 98 161 288 Число двоичных разрядов показателя 64 93 88 87 91 105 135 197 324 128 190 176 170 170 181 209 271 396 512 766 704 666 644 640 656 709 828 768 1150 1056 996 960 945 954 1001 1116 1024 1534 1408 1327 1276 1251 1252 1294 1404 2048 3070 2816 2650 2540 2473 2444 2463 2555 4096 6142 5632 5295 5068 4918 4828 4801 4858 I М-арный алгоритм возведения в степень с сокращенной таблицей предвычислений 1. Вычислить и запомнить таблицу значений a3 mod ш, a5 mod m, mod m. 7 2k- a mod /7i, ..., a 2. Если en_i = 0, то положить p <— 1. Если <?„_i ^ 0, то представить e„_i в виде en.\ = 2'м, где u - нечет-1 ное. Вычислить р <— dl mod m. В обоих случаях положить / <— п - 2. 3. Если ev = 0, то положить Р^Р modws вычислив \р2) J ••• mod ш (к-кратное возведение в квадрат по модулю J. Если ех Ф 0, то представить е-х в виде et = 2'м, где и - нечетное; 2| положить р <r- p mod/л, затем /? <— pa" mod ш и, наконец, р <— р mod w . 4. Положить / <— / - 1; при / > 0 вернуться на шаг 3. 5. Результат:/?. Весь секрет этого алгоритма состоит в разделении операций возведения в квадрат на шаге 3 таким образом, что возведение а в степень регулируется четным делителем 2' числа е-%. Вместе с возведениями в квадрат остается и возведение числа а в нечетную степень и. f Баланс между операциями умножения и возведения в степень смещается в сторону более предпочтительного возведения в степень, причем вычислять и хранить нужно лишь степени числа а с нечетным показателем.
fДАВА 6. Все дороги ведут к... модульному возведению в степень 105 Теперь нам нужно однозначно представить разряд ех показателя в виде в[ = 2'м, где и - нечетное. Чтобы всегда иметь под рукой числа t и м, можно составить таблицу (см., например, таблицу 6.3 для к = 5). es 22 23 24 25 26 27 28 29 30 30 t 1 0 3 0 1 0 2 0 1 0 u 11 23 3 25 13 27 7 29 15 31 ei 11 12 13 14 15 16 17 18 19 20 21 t 0 2 0 1 0 4 0 1 0 2 0 u 11 3 13 7 15 1 17 9 19 5 21 Для вычисления этих значений воспользуемся вспомогательной функцией twofact_l(), которая будет введена в п. 10.4.1. И прежде чем запрограммировать М-арный алгоритм, нам осталось решить всего одну проблему: как, исходя из двоичного представления показателя или представления в системе счисления с основанием Я = 216, быстро перейти к представлению с основанием М = 2к для произвольного к > 0? В этом нам поможет небольшой «трюк» с индексами, позволяющий получить требуемые разряды et представления в системе счисления с основанием М из представления е в системе счисления с основанием В. Итак, пусть (£r-i£/-2---£o)2 - представление показателя е в системе счисления с основанием 2 (оно потребуется нам для определения числа г двоичных разрядов). Пусть (еи-\еи-2---ео)в ~ представление показателя е как числа типа CLINT в системе счисления с основанием В = 216 и (е п-\е'п-2-'е'ъ)м- представление показателя е в системе счисления с основанием М = 2 , к < 16 (М не должно превышать основания В). Представление показателя е в памяти как CLINT-объекта е_1 задается последовательностью значений e_l[i] типа USHORT для I = 0, ..., и + 1: [и + 1], [е0], [е{], ... , 0M_iL [0]- Заметим, что здесь мы добавили ведущий нуль. Пусть / := [-г1] и пусть si '= \J^j и dt := ki mod 16 для / = 0, ...,/. Справедливы следующие утверждения: 1. Число разрядов в представлении (eVie'n-2...е'0)м равно/+ 1, то есть n-l=f. 2. Разряд V/ содержит младший бит разряда е^. Таблииа 6.3. Значения параметров в разложении разрялов показателя в произвеление степени лвойки и нечетного числа qut • * ei 0 1 2 3 4 5 6 7 8 9 10 t 0 0 1 0 2 0 1 0 3 0 1 u 0 1 1 3 1 5 3 7 1 9 5
106 Криптография на Си и C++ в действии шШтт (6.5) 3. Значение d-x указывает позицию младшего бита разряда е'} в es (отсчет позиций начинается с нуля). Если / </и d{ > 16 - к, то не все биты разряда е',• входят в es. ; оставшиеся (старшие) биты разряда е { входят в es+l. Таким образом, интересующий нас разряд ё',- соответствует к младшим двоичным разрядам числа e,+i# + e. Таким образом, для вычисления разряда eh i e {0, ...,/} получаем следующее выражение: е'{ = ((ejfo + 1] | {ej[si + 2] « BITPERDGT)) » dt) & (2* - 1);. Если для простоты положить e_\[sf + 2] <— 0, то это выражение будет справедливо и для / =/. Таким образом, мы нашли эффективный способ доступа к разрядам показателя в его CLINT-представлении (это стало возможным благодаря тому, что в нем они представлены в системе счисления с основанием 2*, к < 16), сэкономив явные преобразования показателя. Теперь число умножений и возведений в квадрат равно (6.6) \L2(k)-2k-l+\\og2el 1 + - кгк и по сравнению с \Х\(к) (см. стр. 102) затраты на предвычисления сократились вдвое. Теперь таблица, задающая наиболее подходящие значения к (таблица 6.4), несколько изменилась. Таблица 6А. Число умножений АЛЯ ТИПИЧНЫХ алин показателя и различных оснований 2к к 1 2 3 4 5 6 7 8 32 47 44 44 46 53 68 99 162 Число двоичных разрядов показателя 64 95 88 85 85 91 105 135 198 128 191 176 168 164 167 279 209 270 512 767 704 664 638 626 626 647 702 768 1151 1056 994 954 931 924 939 990 1024 1535 1408 1325 1270 1237 1222 1232 1278 2048 3071 2816 2648 2534 2459 2414 2401 2429 4096 6143 5632 5293 5062 4904 4798 4739 4732 Начиная с показателя длины 768, наиболее подходящие значения к стали на 1 больше, чем в предыдущей версии алгоритма возведения в степень (см. таблицу 6.2), тогда как число необходимых модульных
ГддВАб. Все дороги ведут к... модульному возведению в степень ^ 07 умножений заметно сократилось. Вероятно, эта процедура в целом более предпочтительна, чем предыдущий вариант. Теперь цичто не мешает нам реализовать алгоритм. Продемонстрируем реализацию рассмотренных принццпов на примере адаптивной процедуры, использующей соответсГвующее оптимальное значение к. Для этого вновь сошлемся на Ко^а fCohel и вслед за ним найдем наименьшее целое к, УДовлетворяющее неравенству (6.7) ,П , ^ *(* +1)22* I I log, e < ■ 02 2ы-к-2' которое выводится из приведенной выше формулы \12(к) Дця числа необходимых умножений и условия [х2(к+1) - \кг(к) > 0. Константа |_log2<?J, определяющая число модульных возведений в квадрат во всех рассмотренных выше алгоритмах, сократилась; остались только «настоящие» модульные умножения, то есть те, где сомц0жители различны. При реализации процедуры возведения в степень с перченным значением к требуется большой объем оперативной памяти для хранения таблицы предвычислений (степеней числа я); Для £ = 8 необходимо около 64 кбайт для 127 переменных типа С[\щ /Это получается в результате умножения (27 - 1) * sizeof(USHoRT) * CLINTMAXSHORT), при этом два автоматически возникаю1дих поля CLINT не учитываются. Для приложений, использующих процесс0рЫ или модели памяти с сегментированной 16-разрядной архитектурой, это уже максимально допустимый предел (по этому поводу см., например, [Dune], глава 12, или [Petz], глава 7). В зависимости от используемой платформы осуществлять д0СТуП к памяти можно по-разному. Память, необходимая для функции глехр5_1(), берется из стека (как и для всех переменных тищ CLINT), тогда как под каждый вызов функции mexpk_l() выделяется динамическая память. Дабы избежать сопутствующего этому увеличения затрат, можно зарезервировать максимально необ)(0дИМуЮ память при однократной инициализации и освободить ее только п0 окончании всей программы. В любом случае можно подчщИть рас. пределение памяти конкретным требованиям и обращать на них внимание в комментариях к соответствующему коду. Еще одно замечание по реализации: всегда рекомендуете^ проверять, достаточно ли для данного приложения основания д^ = 25. Экономия времени при увеличении к оказывается не так у^ велика по сравнению с общим временем вычислений и с тем, чтоб^ 0Прав- дать большие расходы памяти и, соответственно, затраты % ее рас_ пределение. Типичные оценки времени, затрачиваемого раздичными алгоритмами возведения в степень, приведены в Прило>^ении d
108 Криптография на Си и C++в действии Исходя из этих оценок, можно решать, каким из указанных алгоритмов пользоваться. Для М = 25 алгоритм реализован в виде функции mexp5_l() в пакете ■■{,.« FLINT/C. Макрос EXPJ_() позволяет установить используемую функцию возведения в степень: mexp5_l() или mexpkJO с переменным значением к. Функция: Синтаксис: Вход: Выход: Возврат: Модульное возведение в степень int mexpkj (CLINT bas_l, CLINT expj, CLINT pj, CLINT mj); basj (основание) expj (показатель) m_l (модуль) p_l (вычет по модулю mj) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 E_CLINT_MAL, если функция malloc() выдала ошибку Начинаем с построения таблицы, в которой записаны значения ej = 2*u, где и - нечетное, 0 < е; < 28. Таблица представляется в виде двух векторов. Первый вектор twotab[] содержит показатели t числа 2*, элементами второго - oddtabf] - являются нечетные множители и разряда 0 < ej < 25. Целиком таблица, разумеется, хранится в исходном коде FLINT/C. static inttwotab[] = {0,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5, ...}; static USHORToddtab[] = {0,1,1,3,1,5,3,7,1,9,5,11,3,13,7,15,1,17,9,19,5,21,11,23,3,25,13,...}; int mexpkj (CLINT basj, CLINT expj, CLINT pj, CLINT mj) { В описаниях зарезервирована память под показатели степени I плюс ведуший нуль. Кроме того, необходимо будет еше выделить I указатель dint **aptrj, который будет содержать указатели на 1 вычисляемые степени переменной basj. В accj будут храниться I промежуточные результаты. I
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 109 CLINT a J, a2_l; dint eJ[CLINTMAXSHORT + 1]; CLINTD асе J; dint **aptrj, *ptr_l; int noofdigits, s, t, i; unsigned int k, Ige, bit, digit, fk, word, pow2k, k_mask; Затем выполняется обычная проверка деления на 0 и приведения по модулю 1. if (EQZ_L (mj)) { return E_CLINT_DBZ; } if (EQONE.L (mj)) { SETZERO_L (pj); return E CLINT_OK; /* Модуль = 1 ==> Остаток = 0 */ } с Копируем основание и показатель в рабочие переменные a_l и e_l и убираем все ведушие нули. :;( ] л / cpyj (a_l, bas_l); cpyj (e_l, exp_l); if (EQZ_L (ej)) { SETONE_L (pj);
110 Криптография на Си и C++ в действии return E_CLINT_OK; if (EQZ_L (a_l)) { SETZERCLL (pj); return E_CLINT_OK; } Далее определяем оптимальное значение к; значения 2к и 2к-1 хранятся в pow2k и в k_mask соответственно. Для этого используем функцию ld_l(), которая возвращает число двоичных разрядов аргумента. Ige = ld_l (e_l); к = 8; while (к> 1 &&((к-1)*(к«((к-1)« 1))/((1 « к) - к- 1)) >= Ige- 1) { -к; } pow2k = 1U « к; k_mask = pow2k - 1U; ч г Выделяем память под указатели на степени величины a_l. Основание a_l приводится по модулю m_l. if ((aptrj = (dint **) malloc (sizeof(clint *) * pow2k)) == NULL) { return E_CLINT_MAL; modj (aJ, mj, aj); aptrj[1] = aj;
гдАВА 6. Все дороги ведут к... модульному возведению в степень 111 / ^ "1 ( При к> 1 выделяем память под таблицу предвычислений. При к = 1 этого делать не нужно, поскольку тогда никаких предвычислений не требуется. В приведенных ниже присваиваниях указателю aptr_l[i] следует помнить, что при сложении смешения с указателем компилятор сам правильно масштабирует результат, так как он оперирует с объектами типа «указатель на р». Как уже отмечалось, оперативную память можно выделять и при однократной инициализации. В этом случае указатели на CLINT- объекты должны содержаться в глобальных переменных вне функции или в переменных класса static в функции mexpk l(). if(k>1) { if ((ptrj = (dint *) malloc (sizeof(CLINT) * ((pow2k»1)-1)))==NULL) { return E_CLINT_MAL; } fefe >'«*M'A \п л5> ; aptr_l[2] = a2_l; for (aptr_l[3] = ptrj, i = 5; i < (int)pow2k; i+=2) aptr_l[i] = aptr_l[i - 2] + CLINTMAXSHORT; } Теперь выполняем предвычисления степеней переменной а, хра- нимой в a_l. Вычисляются значения а3, а5, а7, ..., а (а2 играет лишь вспомогательную роль). msqrj (aj, aptr_l[2], m_l); for (i = 3; i < (int)pow2k; i += 2) { mmulj (aptr_l[2], aptr_l[i - 2], aptrj[i], mj); } }
112 Криптография на Си и C++ в действии п О ) 'Г' На этом кончаются отличия случая к > 1. К показателю добавляется нуль в старшем разряде. *(MSDPTR_L(e_l) + 1) = 0; Определяем значение f (представленное переменной noofdigits). noofdigits = (Ige- 1)/k; fk = noofdigits * k; Аля разряда е\ определяем слово st (переменная word) и бит dj (переменная bit). word = fk » LDBITPERDGT; Л fk div 16 V bit = fk & (BITPERDGT-1U); /* fk mod 16 7 Вычисляем разряд en_! по приведенной выше формуле; еп_! представлен переменной digit. switch (k) { case 1: case 2: case 4: case 8: digit = ((ULONG)(eJ[word + 1]) » bit) & k_mask; break; default: digit = ((ULONG)(e_l[word + 1] | ((ULONG)e_l[word + 2] « BITPERDGT)) » bit) & k_mask; } I
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 113 if (digit != 0) Л k-digit > 0 V cpy_l (accj, aptr_l[oddtab[digit]]); К: Вычисляем р ; значение t устанавливаем по таблице twotablen^] - число 2 в максимальной степени, деляшей еп_л; число р представлено переменной accj. г 1 t = twotab[digit]; for (; t > 0; t--) { msqrj (acc_l, acc_l, m_l); } } else /* k-й разряд == 0 7 { SETONE_L (accj); } Umka no noofdigits, начиная с f - 1. for (--noofdigits, fk -= k; noofdigits >= 0; noofdigits-, fk -= k) { Аля разряда е; определяем слово s; (переменная word) и бит d; (переменная bit). word = fk » LDBITPERDGT; /* fk div 16 7 bit = fk & (BITPERDGT - 1U); /* fk mod 16 */
114 Криптография на Си и C++ в действии Щ Вычисляем разряд е; по приведенной выше формуле; е; представлен переменной digit. switch (к) { case 1: case 2: case 4: case 8: digit = ((ULONG)(e_l[word + 1]) » bit) & k_mask; break; default: digit = ((ULONG)(e_l[word + 1] | ((ULONG)e_l[word + 2] « BITPERDGT)) » bit) & k_mask; ДОЧК* Чк * Гш I ) h rwt> »* ** Проходим шаг 3 алгоритма для случая digit = е; Ф 0; значение t устанавливаем по таблице twotab[ei]. if (digit != 0) Л k-digit > 0 */ { t = twotab[digit]; T-'u Вычисляем р а в accj. Аля вычисления au определяем нечетный делитель и разряда е; по таблице aptr_l[oddtab[ej]]. for (s = к — t; s > 0; s-) { 1 msqrj (accj, accj, mj); mmulj (accj, aptr_l[oddtab[digit]], accj, mj);
6. Все дороги ведут к... модульному возведению в степень 115 *s? 2' Вычисляем р ; значение р по-прежнему представлено переменной асе I. for (; t > 0; t~) { msqrj (accj, accj, mj); } } else /* k-dlgit == 0 */ { for (s = k; s > 0; s--) { msqrj (accj, accj, mj); } Шаг 3 алгоритма для случая ej = 0: вычисляем р } Цикл заканчивается; результат accj является степенью по модулю m I. еру J (pj, accj); И наконец, освобождается выделенная память. free (aptrj); if (ptrj != NULL) free (ptrj); return E_CLINT_OK; }
1 116 Криптография на Си и C++ в действии Поясним алгоритм М-арного возведения в степень на численном | примере. Для этого вычислим 1234667 mod 18577 с помощью функ- [ ции mexpk_l(). I 1. Предвычисления Представим показатель е = 661 в системе счисления с основанием 2к с к = 2 (см. М-арный алгоритм возведения в степень на стр. 102), получим е = (1010 011011)2,. Значение a mod 18577 равно 17354. Больше никаких степеней числа а вычислять не требуется: 2к- 1 = 3. 2. Основной цикл Разряд показателя е,-=2'и р <— р2 mod п р <r- p2 mod л р <— p-au mod n р <— р2 mod n 24 - - 1234 18019 24 14132 - 13662 7125 2°-1 13261 4239 10789 - 24 17616 - 3054 1262 2°-3 13599 17343 4445 , - 1 3. Результат 1 р = 1234667 mod 18577 = 4445. ■•*w Рассмотрим частный случай возведения в степень, когда показатель является степенью двойки: 2к. Как мы видели ранее, это легко можно сделать путем /:-кратного возведения в квадрат. Показателю к в 2к будет соответствовать переменная к. Модульное возведение в степень в случае, когда показатель является степени двойки int mexp2_l (CLINT a_l, USHORT k, CLINT p_l, CLINT mj); a_l (основание) k (показатель к в 2к) mj (модуль) pj (вычет а_12 по модулю mj) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 Функция: Синтаксис: Вход: Выход: Возврат:
6. Все дороги ведут к... модульному возведению в степень 117 int mexp2_l (CLINT aj, USHORT k, CLINT pj, CLINT mj) { CLINT tmpj; if (EQZ_L (mJ)) { return E_CLINT_DBZ; } При k > 0 возводим к раз aj в квадрат по модулю mj. R H'GiP: if (k > 0) { cpyj (tmpj, aj); while (k- > 0) { msqrj (tmpj, tmpj, mj); } cpyj (pj, tmpj); } else В противном случае, при k = 0, нужно выполнить лишь приведение по модулю m_l. { rnodj (aj, т_1, р_1); } return E_CLINT_OK; }
118 Криптография на Си и C++ в действии 6.3. Аддитивные цепочки и окна На сегодняшний день опубликовано множество алгоритмов возведения в степень: одни удобны лишь для частных случаев, другие универсальны. Но цель всегда одна и та же - по возможности сократить число умножений и делений, как это было при переходе от бинарного к М-арному алгоритму. Алгоритмы бинарного и М-арного возведения в степень, в свою очередь, являются частными случаями аддитивных цепочек (см. [Knut], п. 4.6.3). Мы уже знаем, что при возведении в степень можно представить показатель в виде суммы: е = к + / => ае = аы = ака. Представляя показатель в двоичной системе счисления: 2к-1 . г\к-2 . , + ек-2 • 2 + ... + е0, можно выполнить возведение в степень с помощью возведений в^ квадрат и умножений (см. стр. 96): ( (,,. ^ ^2^ ^ ае mod/i: (ае^})ае^ ае° modn. Элементами соответствующей аддитивной цепочки являются показатели при степенях числа а: ек-\, ек-\' 2, ек-\- 2 + ек.ъ (ек.{- 2 + ек_.2У 2, (ек-{- 2 + ек.2У 2 + ^_3, ^ , ((е*-Г 2 + ек.2У 2 + ек_3У 2, ;; (•••(е*-Г 2 + е*_2)' 2 + - + ^i)- 2 + e0. Если для некоторого значения j показатель е,- = 0, то соответствующие элементы последовательности опускаются. Например, для числа 123 результатом бинарного метода будет аддитивная цепочка из 12 элементов: 1, 2, 3, 6, 7, 14, 15, 30, 60, 61, 122, 123. В общем случае последовательность чисел 1 = а0, дь аъ ..., аг = е, в которой для каждого / = 1, ..., г существует пара чисел (/, ^) таких, что j < к < i и а, = aj + ah называется аддитивной цепочкой длины г для числа е. М-арный метод обобщает представление показателя на случай произвольного основания. Цель у обоих методов общая - получить наиболее короткие аддитивные цепочки и тем самым снизить вычисли-
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 119 тельные затраты на возведение в степень. Для числа 123 23-арный метод дает аддитивную цепочку I, 2, 3, 4, 7, 8, 15, 30, 60, 120, 123; 24-арный метод - 1,2,3,4,7, 11, 14,28,56, 112, 123. Эти две цепочки значительно короче, чем цепочка в бинарном методе; для больших чисел разница будет еще более значительной. Однако, раз уж мы говорим о временных затратах, следует отметить, что база данных а , а , а , ..., а ~, которую мы строим при инициализации М-арных методов, включает в себя и те степени я, которые не нужны для представления е по основанию М или для построения аддитивной цепочки. Наихудшим случаем построения аддитивной цепочки является бинарное возведение в степень: здесь цепочка имеет максимально возможную длину log2e + Н(е) - 1, где через //О) обозначен хем- мингов вес числа е. Снизу длина аддитивной цепочки ограничена числом log2e + log2#(6?) - 2,13, более коротких цепочек быть не должно (см. [Scho] или [Knut], п. 4.6.3, упражнения 28, 29). В нашем случае это означает, что длина самой короткой аддитивной цепочки для е = 123 не может быть меньше 8, г* значит, приведенные выше результаты Л/-арных методов далеко не самые лучшие. До сих пор не существует полиномиального алгоритма, решающего задачу поиска кратчайшей аддитивной цепочки. Эта задача принадлежит классу сложности NP, то есть относится К задачам, которые могут быть решены за полиномиальное время недетерминированными методами. Иначе говоря, решение этих задач за полиномиальное время можно лишь «угадать», в отличие от задач класса Р, которые будут решены детерминированно. Неудивительно, что Р является подмножеством NP, поскольку все задачи, решаемые за полиномиальное время детерминированными методами, могут быть решены за то же время недетерминированными методами. Определение кратчайшей аддитивной цепочки является NP-полной задачей, то есть задачей, сложность которой не хменьше, чем сложность любой другой задачи из класса NP (см. [HKW], стр. 302). NP- полные задачи тем более интересны, что если хотя бы одна из них будет решена детерминированными методами за полиномиальное время, то и все остальные задачи из NP будут решены за полиномиальное время. В этом случае можно будет сказать, что классы Р и NP - это одно и то же множество задач. Задаче о том, совпадают ли множества Р и NP, является основной нерешенной задачей теории сложности. Однако в настоящее время превалирует мнение о том, что Р ^ NP. Если число п в двоичной системе счисления имеет вид п = (nk_ink-2- • -По)Я> то Н(п) = / jPi (см. [HeQu], Глава 8).
1 120 Криптография на Си и C++ в действии Теперь ясно, что при практической реализации процедуры генерации аддитивных цепочек мы должны опираться на некие эвристики, то есть те приближенные математические методы, которые в данном случае работают эффективнее других: как, например, в случае с определением показателя к для 2*-арного возведения в степень. Например, в 1990 г. Я. Якоби (Y. Yacobi) [Yaco] установил связь между построением аддитивных цепочек и сжатием данных по методу Лемпеля-Зива (Lempel-Ziv); в его работе приведен также алгоритм возведения в степень, основанный на таком сжатии и М-арном методе. Для поиска кратчайшей аддитивной цепочки возможно дальнейшее обобщение М-арного метода возведения в степень, чем мы сейчас и займемся. В методах окна, в отличие от М-арного метода, показатель представляется не разрядами в системе счисления по фиксированному основанию М, а разрядами переменной двоичной длины. Так, например, разрядом показателя может быть длинная последовательность двоичных нулей, называемая нулевым окном. Вспомним М-арный алгоритм со стр. 104: ясно, что для нулевого окна длины / потребуется /-кратное возведение в степень, то есть третий ov шаг алгоритма примет вид: 2 3. Положить р <— р modm = и 2 Л2 mod m. (/ раз) Ненулевые разряды обрабатываются либо как окна фиксированной длины, либо как изменяемые окна максимальной длины. Как и в М-арном случае, для любого ненулевого окна (называемого далее, не совсем удачно, «1-окном») длины t помимо повторного возведения в квадрат выполняется еще дополнительное умножение на некоторый элемент таблицы предвычислений: У. Положить р <— р modm, а затем р <— pa€i modm. Число элементов таблицы предвычислений зависит от допустимой максимальной длины 1-окна. Отметим, что младший разряд 1-окна всегда равен 1, то есть 1-окно всегда нечетно. Таким образом, нам не надо здесь раскладывать разряд показателя, как на стр. 104, на четный и нечетный множители. С другой стороны, при возведений в степень мы двигаемся слева направо, а это значит, что, прежде чем возводить в степень, нам потребуется полностью разложить показатель на множители и, кроме того, помнить это разложение. Тем не менее, если мы начнем раскладывать показатель со старшего разряда и будем двигаться слева направо, то можно обрабатывать каждое 0- или 1-окно сразу же по мере заполнения. Отсюда, оче* видно, следует, что у нас получатся и четные 1-окна, но этот случай алгоритмом допускается. А
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 121 По существу, разложение показателя на 1-окна и в том, и в другом направлении выполняется одним и тем же алгоритмом. Сформули-^ руем его для разложения справа налево. Разложение целого числа е на 0- и 1-окна фиксированной длины I 1. Если младший двоичный разряд числа е равен 0, то начать 0-окно и перейти на шаг 2; в противном случае начать 1-окно и перейти на шагЗ. 2. Пока не появится 1, добавлять следующие по старшинству двоичные разряды в 0-окно. Как только появится 1, закрыть 0-окно, начать 1-окно и перейти на шаг 3. 3. Собрать следующие / - 1 двоичных разрядов в 1-окне. Если последующий разряд равен 0, то начать 0-окно и перейти на шаг 2; в противном случае начать 1-окно и перейти на шаг 3. Алгоритм заканчивает работу после того, как пройдены все разряды числа е. пц '■ При разложении слева направо начинаем со старшего двоичного разряда и действуем по аналогии. Если предположить, что в числе е нет начальных нулей, то алгоритм не может закончиться на шаге 2, а только на шаге 3. Приведем два примера. Пусть е = 1896837 = (111001111000110000101)2 и 1 = 3. Раскладываем е, начиная с младшего двоичного разряда: е = Ш 001 Ш_ 00 Ш 0000 НИ. При / = 4 получаем разложение е = Ш 00 ПЦ 0 ООН 000 0101. Рассмотренный выше 2^-арный метод при к = 2 дает разложение е = ШД001Ц10001Ш000101. Таким образом, при / = 3 получаем в разложении числа е пять 1-окон, а при / = 4 только четыре; в обоих случаях требуется одно и то же число дополнительных умножений. Разложение по 22-арному методу содержит восемь 1-окон, требует в два раза больше дополнительных умножений, чем в случае / = 4 и, значит, вряд ли заслуживает внимания. При выполнении той же процедуры слева направо, начиная со старших разрядов, при / = 4 и е = 123 получаем разложение е = 1110 0 1111 000 1100 00101, также с четырьмя 1-окнами, среди которых, как уже отмечклось, есть четные. Теперь мы, наконец, можем сформулировать алгоритм разложения показателя методом окон с учетом обоих направлений разложения.
122 Криптография на Си и C++ в действц .4:. Алгоритм вычисления вычета ае mod m с разложением показателя е на нечетные 1-окна (максимальной) длины / 1. Разложить показатель е на 0- и 1-окна (C0k_i...coo) длины /*_;, ...,| соответственно. 2. Вычислить и запомнить аъ mod /л, as mod /я, a7 mod ш, ..., я2] mod т. 3. Положить Р <~ <*щ-1 mod ш и / <- & - 2. 4. Положить P^~Pl modm. О), 5. Если О} * 0, то положить Р <r~ Ра mod т. 6. Положить /<—/-!; при / > 0 вернуться на шаг 4. 7. Результат: р. Если среди 1-окон есть четные, то вместо шагов 3-7 выполняются^ следующие: 3'. Если со^_! = 0, то положить 21к~х 1 р <— р mod ш = 2 Л -ГС^Т- mod m. Ok-i Ъ) Если соы * 0, то представить щ_х в виде coA_i = 2'и, где и - нечетное; положить р <r-au mod ш, а затем р <— /?2 mod m . В обоих случаях положить i <г- к-2. 4'. Если со, = 0, то положить ч2 р <— /? mod ш: -Г^П2-' mod m. /, а^ Если со, ^ 0, то представить со, в виде со, = 2*и, где w - нечетное; положить р <г- р mod т, затем р <- раи mod ш и, наконец, р <r- p mod /и . 5'. Положить / <— / - 1; при / > 0 вернуться на шаг 4'. 6'. Результат: р.
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 123 6 4. Приведение по модулю и возведение в степень методом Монтгомери Теперь оставим аддитивные цепочки и обратимся к другому подходу, интересному, прежде всего, с точки зрения алгебры. Этот подход позволяет заменить умножение по модулю нечетного числа п умножением по модулю 2*, которое не требует деления в обычном понимании и, следовательно, является более эффективным, чем приведение по модулю произвольного числа п. Этот замечательный метод был опубликован в 1985 г. П. Монтгомери (P. Montgomery) [Mont] и с тех пор широко применяется. В основе метода лежит следующее свойство. Пусть п и г - взаимно простые целые числа; г"1 мультипликативно обратно к г по модулю п\ п~х мультипликативно обратно к п по модулю г. Пусть п := -/Г1 mod г и т := tn mod г. Для любого целого / справедливо сравнение CIM-- (6.8) t + mn ч 1 ' = Гг mod n Заметим, что при вычислении левой части сравнения мы оперируем со сравнениями по модулю г (поскольку / + тп = 0 mod r, остаток от деления на г равен нулю), но не по модулю п. Если выбрать в качестве г степень двойки 2s, то s младших битов числа х и будут задавать остаток от деления х на г, а деление jc на г выполняется простым сдвигом числа х на s бит вправо. Таким образом, вся прелесть сравнения (6.8) состоит в том, что его левая часть вычисляется значительно быстрее, чем правая. Здесь нужны всего две операции, для выполнения которых можно воспользоваться функциями mod2_l() (см. п. 4.3) и shiftJO (см. п. 7.1). Такая процедура вычисления вычета по модулю п называется преобразованием Монтгомери. Поскольку здесь требуется, чтобы числа п и г были взаимно простыми, число п должно быть нечетным. Ниже мы покажем, что с помощью преобразования Монтгомери можно выполнять модульное возведение в степень значительно быстрее, чем предыдущими методами. А пока уточним некоторые моменты. Корректность сравнения (6.8) можно проверить довольно просто. Подставим в левую его часть вместо т значение tn mod r (см. формулу (6.9)), затем заменим tn mod г на tn -r[tn/r\e Z (получим (6.10)), наконец, выразим п в виде целого числа (/г- 1)1 п для некоторого г е Ж и получим (6.11). Результатом приведения по модулю п будет формула (6.12): (6-9) t + тп __ t + n(tn mod r) г
124 Криптография на Си и C++ в действц (6.10) t + ntn tn г (6.11) t + t(rr'-\) (6.12) (6.13) (6.14) = tr l mod n. Пусть /z, t, r e Z, НОД(/г, r) = 1 и n := -n~ mod г. Тогда из формулы (6.8) следует, что для ДО := t + (tn mod r) n справедливо fit) = / mod n, (6.15) .Do ДО s 0 mod r. К этому результату мы еще вернемся. Чтобы применять преобразование Монтгомери, будем проводить вычисления по модулю п в полной системе вычетов (см. главу 5): R := R(r, n) := {ir mod n\0<i< n] ,5-1 с подходящим г := 2 > 0, таким, что 2Л < л < 2Л. Определим произведение Монтгомери «х» чисел а и b из 7? как axb := абг"1 mod n, где через г"1 обозначено число, мультипликативно обратное к г по модулю п. Имеем axb = (ir)(jr)r~l = (ij)r mod n e R, то есть произведение х двух элементов множества R тоже принадлежит множеству R. Произведение Монтгомери вычисляется с помощью преобразования Монтгомери. Поскольку числа п и г взаимно просты, расширенным алгоритмом Евклида (см. п. 10.2) получаем линейное представление их наибольшего общего делителя: 1 = НОД(и, г) = г г - п'п, где п := -/f1 mod r. Тогда из линейного представления : 1 = г г mod n и 1 == -ц'п mod r,
6. Все дороги ведут к... модульному возведению в степень 125 то есть число г = г"1 mod п мультипликативно обратно к г по модулю п, а число п = -n~l mod г, взятое со знаком «-», мультипликативно обратно к п по модулю г (здесь мы немного забежали вперед; см. п. 10.2). Произведение Монтгомери вычисляется по следующему алгоритму. Вычисление произведения Монтгомери а X Ъ в R(r9 n) 1. Положить t <— ab. 2. Вычислить т <— tn'mod г. 3. Вычислить и <— (t + mri) I r (деление выполняется нацело; см. выше). ш 4. Если и > п, то результат: и-п; иначе результат: и. Параметры алгоритма выбраны так, что a, b < п и /и, п < г, тогда и < Ъг (см. формулу (6.21)). Этот алгоритм требует вычисления трех произведений больших чисел: iг. одно на шаге 1 и два на этапе приведения (шаги 2, 3). Поясним алго- * 4l ритм на маленьких числах. Пусть а = 386, Ъ = 257, п = 533. выберем г = 210. Тогда п = -n~l mod г = 707, т = 6, t + тп = 102400 ии= 100. !ГГ-'"^ f * Теперь можем вычислять произведение ah mod л, где число п нечетное, следующим образом. Сопоставим числам а и Ъ элементы множества R: а <— ar mod n, b' <— br mod /z, затем найдем произведение Монтгомери р <— а х 6' = a'/jV-1 mod я и, наконец, выполним обратное преобразование: р <— р х 1 -pr~x = ab mod л. Можно обойтись и без обратного преобразования, если сразу вычислить р <r- a xb, тем самым избавившись и от необходимости преобразовывать Ъ. В результате получаем следующий алгоритм. Вычисление р = ab mod n (для нечетного я) с помощью произведения Монтгомери 1. Подобрать г := 2s такое, что 2s-l <n< 2s. Найти линейное представление 1 = r'r-п'п расширенным алгоритмом Евклида. 2. Положить a'<r- ar mod п. 3. Вычислитьр <r- a'xb, результат: р. L Опять поясним алгоритм на маленьких числах. Пусть а = 123, L Ъ =456, п = 789, г = 210. Тогда n=-n~l modr= 963, я'= 501 и |'{: р = а х Ъ = 69 = ab mod n. I Вычисление значений / и п на шагах 1, 2 и вычисление произведе- [ 4 ния двух больших чисел все же требуют значительно больших временных затрат, чем «обычное» модульное умножение, поэтому при
126 Криптография на Си и C++ в действии однократном модульном умножении алгоритмом Монтгомери пользоваться не стоит. Когда же нам нужно вычислить много модульных произведений по одному и тому же модулю, то есть когда указанные трудоемкие предвычисления выполняются всего один раз, результаты более впечатляющие. Алгоритм Монтгомери особенно хорош для модульного возведения в степень, нужно лишь немного изменить М-арный алгоритм. Опять представим показатель е и модуль п в системе счисления с основанием В = 2к: е = (ет-\ет-2--.ео)в и п = (п1-\П1_г--.щ)в. Следующий алгоритм вычисляет степени ае mod n в кольце Жп, где п нечетное, с помощью произведения Монтгомери. Возведение в квадрат сводится к вычислению а х а. Возведение в степень по модулю п (для нечетного п) с помощью произведения Монтгомери 1. Положить г^-В! = 2к1. Найти линейное представление \=г'г-п'п расширенным алгоритмом Евклида. 2. Положить ~а <— ar mod п. Вычислить и запомнить а3,я5, ...,а2 _1 с помощью произведения Монтгомери х в R(r, n). 3. Если ет-1 Ф О, то найти разложение em.j - 2*и, где и нечетное. Положить р <— \аи J . Если ет.\ = 0, то положить р <— г mod n . В любом случае положить i <— т - 2. .. ( //-_,** v „ (к раз воз- *-•...№■■■ 4. Если et = 0, то положить р <r- p вести в квадрат: р = ~р х ~р). Если в[ Ф 0, то найти разложение е{ = 2*и, где и нечетное. Поло- - (-2^ -и}' жить р <г-\р х а ) . 5. При / > 0 положить / <— / - 1 и вернуться на шаг 4. 6. Результат: произведение Монтгомери Р х 1. Дальнейшее усовершенствование этого алгоритма возможно, скорее, за счет модификации алгоритма умножения Монтгомери, чем алгоритма возведения в степень, что и сделали СР. Дуссе (S.R. Dusse) и Б.С. Калиски (B.S. Kaliski) в работе [DuKa]. Вычисляя произведение Монтгомери алгоритмом со стр. 125, можно избежать выполнения присваивания т <— tn mod r на шаге 2. Кроме того, при выполнении преобразования Монтгомери можно оперировать с
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 127 п0 := п mod В, а не с п. Вычислим разряд пц <— fyi'0 mod В, умножим его на п, затем на В1 и прибавим результат к t. Чтобы найти произведение чисел a, b <п по модулю /г, представим, как и рань- ! ], ше, п = (/2/_i/i/_2.. .«o)^ и положим г := £;, г/ - пп = 1 и «'о := л' mod В. . Алгоритм Дуссе и Кал иски вычисления произведения Монтгомери а х Ь 1. Положить t <— ab, по <— л'mod 5, / <— 0. 2. Вычислить mi <— Г/По niod В (\щ будет одноразрядным целым числом). 3. Положить t <r-t + 1щгВ\ , г >iii 4. Положить i <— i + 1; при / < / - 1 вернуться на шаг 2 5. Положить t <r- t / г. в ^^ 6. Если г > п, то результат: г - п; иначе результат: 7. В работе Дуссе и Калиски утверждается, что рассмотренное упрощение основано на том, что t рассматривается как кратное числа г, однако доказательство не приводится. Прежде чем использовать эту процедуру, уточним, почему она действительно вычисляет произведение а х Ъ. Следующие рассуждения основаны на результатах Кристофа Бурникеля [Zieg]. На шагах 2 и 3 алгоритма вычисляется последовательность (f(l))/=o / по рекурсивной формуле: (6.16) ti0) = ab, (6.17) (6.20) ft(»\ В' £',i = 0,...,/-l, где № = *+ (0 mod В) (-/Г1 mod В) mod В) п - уже знакомая нам функция (см. формулу (6.13) при /*<—#)• Элементы последовательности t0) обладают следующими свойствами: t{i) s 0 mod B\ t(l) = ab mod n, = abr l mod n,
1 128 Криптография на Си и C++ в действиц (6.21) ль < 2/г. Гч&\\у. Свойства (6.18) и (6.19) следуют непосредственно из (6.14)—(6.17); из (6.18) получаем В1 \t(l) <^> r\t{l). Отсюда и из сравнения /(/) = ab mod n следует (6.20). Неравенство (6.21) выводим из соот- ношения *(/)=*<°>+,£>,*'<2нЯ' /=о поскольку f(0) = ab < п2 < пВ1. Теперь скорость приведения по модулю определяется скоростью умножения чисел, по порядку величины близких к модулю. Такой вариант умножения по Монтгомери можно элегантно реализовать с помощью функции, аналогичной mulj (см. стр. 49). Функция: Синтаксис: Вход: Выход: Умножение по Монтгомери void mulmonj (CLINT a J, CLINT bj, CLINT nj, USHORT nprime, USHORT logB_r, CLINT pj); a_l, bj (сомножители aub) nJ (модуль /i >a,b) nprime (n mod B) logB_r (логарифм числа г по основанию В = 216; должно выполняться неравенство в"09В-г"1 < п < ^'^в-г) pj (произведение Монтгомери axb = а • Ь • г"1 mod n) void mulmonj (CLINT a J, CLINT bj, CLINT nj, USHORT nprime, USHORT logB_r, CLINT pj) { CLINTD tj; dint *tptr_l, *nptr_l, *tiptrj, *lasttnptr, *lastnptr; ULONG carry; USHORT mi; int i; mult (aj, bj, tj); lasttnptr = tj + DIGITS_L (nJ); lastnptr = MSDPTR_L (nj); i
т ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 129 Используя функцию mult(), мы гарантируем отсутствие переполнения (см. стр. 86) при вычислении произведения чисел a_l и b_l. Для возведения в квадрат по Монтгомери используем sqr(). Результат записывается в t_l, где места для него достаточно. Затем t_l дополняется нулевыми старшими разрядами до длины, в два раза большей, чем длина n_l. for (i = DIGITS_L (t_l) + 1; j <= (DIGITS_L (nj) « 1); i++) { U[i] = 0; SETDIGITS_L (tj, MAX (DIGITS_L (tj), DIGITS_L (nj) « 1)); При выполнении следующего двойного иикла последовательно вычисляются и складываются с t_l частичные произведения гг^пВ1, где rrij := tjn'0. Здесь тоже текст программы аналогичен функции умножения. for (tptrj = LSDPTR_L (t_l); tptrj <= lasttnptr; tptr_l++) { carry = 0; mi = (USHORT)((ULONG)nprime * (ULONG)*tptr_J); for (nptrj = LSDPTFLL (nj), tiptrj = tptrj; nptrj <= lastnptr; nptr_l++, tiptr_l++) I 0 *tiptr_l = (USHORT)(carry = (ULONG)mi * (ULONG)*nptrJ + (ULONG)*tiptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); } В следующем внутреннем цикле при возникновении переполнения перенос записывается в старший разряд переменной tj, поэтому t_l и содержит один дополнительный разряд. Этот шаг очень важен, поскольку в начале основного цикла переменной tj присваивалось значение, в отличие от переменной р_1, которая была инициирована путем умножения на 0.
130 Криптография на Си и C++ в действии for (; ■l I { ((carry » BITPERDGT) > 0) && tiptrj <= MSDPTRJ. (tj); tiptrj++) *tiptrj = (USHORT)(carry = (ULONG)*tiptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); } if (((carry » BITPERDGT) > 0)) { *tiptrj = (USHORT)(carry » BITPERDGT); INCDIGITSJ. (tj); } } Далее следует деление на В1, для чего мы сдвигаем tj на logB_r бит вправо, т.е. отбрасываем младшие logB_r бит переменной tj. Затем модуль п_1 при необходимости вычитается из tj, и tj возвращается в р_1 в качестве результата. tptrj = tj + (1одВ_г); SETDIGITS_L (tptrj, DIGITS_L (tj) - (1одВ_г)); if (GE_L (tptrj, n_l)) { sub J (tptrj, nj, pj); + i_ 'I else { cpyj (pj, tptrj); } i Функция sqrmonJO возведения в квадрат по Монтгомери несущественно отличается от только что рассмотренной: в вызове функции I
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 131 нет переменной bj, а вместо функции умножения mult(a_l, bj, t_l) используется функция sqr(a_l, bj), точно так же пренебрегающая { возможным переполнением. Однако здесь следует отметить, что | при возведении в квадрат по Монтгомери вслед за вычислением 1 // <— я' х а выполняется обратное преобразование /?<—// х 1 = р'г~{ = I = a2 mod п (см. стр. 125). Функция: Синтаксис: Вход: Выход: Возведение в квадрат по Монтгомери void sqrmonj (CLINT a_l, CLINT n_l, USHORT nprime, USHORT logB_r, CLINT pj); aj (множитель а) nJ (модуль п > a) nprime (n mod B) logB_r (логарифм от г по основанию В = 216; должно выполняться неравенство в1098-'"1 <п< #logB-r) р J (вычет а2 • г"1 по модулю п) В своей статье Дуссе и Калиски приводят также следующий вариант расширенного алгоритма Евклида (его мы еще будем рассматривать в п. 10.2) для вычисления /г'о = п mod В, который снижает сложность предвычислений. Этот алгоритм использует арифметику больших чисел для вычисления вычета -/г"1 mod 2\ где s > 0. Алгоритм вычисления обратного значения -л 1 mod 2s, где s > 0, п нечетное 1. Положить х <— 2, у <— 1, / <— 2. 2. Если х < пу mod х, то положить у <— у + х. 3. Положить х <— 2х и / <— / + 1; при i < s вернуться на шаг 2. 4. Результат: х-у. Методом математической индукции можно доказать, что на шаге 2 рассмотренного алгоритма всегда выполняется сравнение уп = 1 mod х и, значит, у = /Г1 mod x. Как только на шаге 3 переменная х примет значение 2Л, мы получаем нужный результат: 2s- у = -п~{ mod 2s, если только выбрать s из условия 2Л = В. Этот алгоритм реализован в виде небольшой функции invmomJO на FLINT/C. Аргументом функции является модуль п, результатом - значение -/f1 mod В. Все эти соображения подтверждаются при построении функций mexp5m_l() и mexpkmj(), для которых мы приводим здесь только интерфейс и численный пример.
132 Криптография на Си и C++ в действии Функция: Синтаксис: Вход: Выход: Возврат: Модульное возведение в степень в случае нечетного модуля (25-арный или 2*-арный метод с умножением по Монтгомери) intmexp5m l(CLINTbas I, CLINT exp I, CLINT p I, CLINT mj); int mexpkmJ(CLINT basj, CLINT exp J, CLINT pj, CLINT mj); basj (основание) expj (показатель) mj (модуль) pj (вычет по модулю m_l) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 E_CLINT_MAL, если функция malloc() выдала ошибку E_CLINT_MOD в случае четного модуля В этих функциях для вычисления произведения Монтгомери используются процедуры invmonJO, mulmon_l() и sqrmon_l(). В основе ■■*.-.- их реализации - функции mexp5_l() и mexpk_l(), модифицированные в соответствии с описанным выше алгоритмом возведения в степень. L Поясним алгоритм возведения в степень по Монтгомери функцией mexpkm_l() на том же численном примере, который мы рассматривали при М-арном возведении в степень (см. стр. 116). Приведем основные этапы вычисления 1234667 mod 18577. 1. Предвычисления Представим показатель е = 667 в системе счисления с основанием 2к с к = 2 (см. алгоритм возведения в степень по Монтгомери на стр. 126), получим е= (1010011011)22. Значение г, используемое в преобразовании Монтгомери, равно г = 216 = Я = 65536. Значение nQ (см. стр. 127) теперь равно п0 = 34703. *,] *2м Преобразуем основание а в элемент системы вычетов ;>:\т■ ч (см. стр. 124): \ a=ar mod n = 1234 • 65536 mod 18577 = 5743. Значение элемента а3 множества /?(г, п) равно а3 = 9227. Показатель мал, поэтому дальнейшие степени числа а вычислять не нужно.
6. Все дороги ведут к... модульному возведению в степень 133 2. Основной цикл Разряд показателя е,= 2*и Р<-Р2 Р<-Р2~ р<г-рхаи Р^-Р2 24 - - 5743 9025 24 16994 - 15740 11105 2°-1 3682 6646 8707 - 24 14511 - 16923 1628 2°-3 11066 12834 1583 - 3. Результат Значение р после обратного преобразования: p = pxl = pr~l mod n = 1583r-1modn = 4445. Читателя, интересующегося подробностями программной реализации функций mexp5mj() и глехркт_1()и численного примера для функции mexpkmJO, мы отсылаем к исходному тексту программы на FLINT/C. В начале главы мы ввели функцию wmexp_l(), удобную в случае малых оснований и требующую только умножений вида р <— pa mod m переменных типа CLINT * USHORT mod CLINT. Эту функцию также можно ускорить, заменив модульное возведение в квадрат аналогичной процедурой по Монтгомери, как мы это делали в mexpkm_l(). Здесь мы будем использовать быструю функцию обращения invmon_l(), а умножение оставим без изменений. Это можно сделать, поскольку при возведении в квадрат по Монтгомери и обычном умножении по модулю п (aV1) Ъ = (а2Ь) г-1 mod n мы остаемся в рамках системы вычетов /?(r, n) = {ir mod n\0<i< n]. В результате получаем две функции: wmexpm_l() и umexpm_l(), аргументами которых являются показатели типа USHORT, для нечетных модулей. Эти функции являются значительно более быстрыми по сравнению с «обычными» функциями wmexpJO и umexpj(). Для них мы снова приводим лишь интерфейс и численный пример, а читателя отсылаем за подробностями к исходному тексту программы на FLINT/C.
134 Криптография на Си и C++ в действии функция: Синтаксис: Вход: Выход: Возврат: Модульное возведение в степень с использованием преобразования Монтгомери для основания (или показателя соответственно) типа USHORT и нечетного модуля int wmexpmj (USHORT bas, CLINT ej, CLINT pj, CLINT mj); intumexpm I (CLINT bas I, USHORT e, CLINT p I, CLINT m I); bas, basj (основание) e, ej (показатель) mj (модуль) pj (вычет baseJ по модулю mj, соответственно вычет bas_le по модулю mj) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 E_CLINT_MOD в случае четного модуля Функция wmexpmj заготовлена специально для алгоритма проверки на простоту из п. 10.5, где мы, наконец-то, пожнем плоды теперешних усилий. Проиллюстрируем эту функцию на уже знакомом нам примере: вычислим 1234667 mod 18577. 1. Предвычисления Двоичное представление показателя: е = (1010011011)2. Значение г, используемое в преобразовании Монтгомери: г=21в = В = 65536. Значение п0 (см. стр. 127) вычисляется, как и раньше: /г'о = 34703. Определяем начальное значение ~р <— pr mod 18577. 2. Основной цикл Двоичный разряд показателя р <г- рхр в R(>\ п) р <r—]5a mod n 1 9805 5743 0 9025 - 1 16994 15740 0 11105 - 0 3682 - 1 6646 8707 1 14511 16923 0 1628 - 1 11066 1349 1 9350 1583 3. Результат ;| Значение р после обратного преобразования: J р = pxl = pr~l mod n = 1583r-1mod/z = 4445. Подробное исследование временной сложности преобразования Монтгомери с учетом различного рода оптимизаций можно найти в работе [Boss]. Мы обещали читателю 10-20%-ный выигрыш в скорости от использования преобразования Монтгомери по сравнению с традиционным возведением в степень. Приложение D, в котором
6. Все дороги ведут к... модульному возведению в степень 135 Таблииа 6.5. функции возвеления в степень в пакете FLINT/C приведены типичные оценки времени для функций на FLINT/C, полностью подтверждает наши обещания. Мы ограничились случаем возведения в степень по нечетному модулю. Тем не менее, для многих прикладных задач, например, при зашифровании и расшифровании, а также при вычислении цифровой подписи по алгоритму RSA (см. главу 16) можно воспользоваться функциями mexp5mj() и mexpkmj(). Подведем итоги. В нашем распоряжении несколько функций модульного возведения в степень. Сведем их в таблицу 6.5 с учетом особенностей и возможных областей применения. Функция mexp5_l() mexpkJO техр5т_1() mexpkmJO umexpj() umexpmJO wmexpJO wmexpmJO mexp2J() Область применения Обшее 25-арное возведение в степень; не использует динамическое распределение памяти; значительные требования к стеку. Обшее 2^-арное возведение в степень, где значение к оптимально для чисел типа CLINT; использует динамическое распределение память- незначительные требования к стеку. 25-арное возведение в степень по Монтгомери для нечетного модуля; не использует динамическое распределение память- значительные требования к стеку. 2^-арное возведение в степень по Монтгомери для нечетного модуля, где значение к оптимально для чисел типа CLINT длиной до 4096 двоичных разрядов; использует динамическое распределение памяти; незначительные требования к стеку. Смешанное бинарное возведение в степень для основания типа CLINT, показателя типа USHORT; незначительные требования к стеку. Смешанное бинарное возведение в степень с использованием преобразования Монтгомери для основания типа CLINT, показателя типа USHORT и только для нечетного модуля; незначительные требования к стеку. Смешанное бинарное возведение в степень для основания типа USHORT, показателя типа CLINT; незначительные требования к стеку. Смешанное бинарное возведение в степень с использованием возведения в квадрат по Монтгомери для основания типа USHORT, показателя типа CLINT и нечетного модуля; незначительные требования к стеку. Смешанное возведение в степень с показателем вида 2е; незначительные требования к стеку.
-I зб Криптография на Си и C++ в действии 6.5. Криптографические приложения модульного возведения в степень На протяжении этой главы мы славно поработали над модульным возведением в степень. Пора бы остановиться и спросить себя: а для чего оно нужно в криптографии? Первое, что приходит в голову, это, несомненно, вычисления в криптосистеме RSA, где показателями степени при зашифровании и расшифровании являются, соответственно, открытый и секретный ключи. Но пусть читатель наберется немного терпения, поскольку для изучения криптосистемы RSA у нас в следующей главе припасено кое-что еще, так что отложим разговор до главы 16. Для самых нетерпеливых приводим два очень важных алгоритма, в » i которых нужно возводить в степень. Это протокол обмена ключами, предложенный в 1976 году Мартином Е. Хеллманом (Martin E. , .„. . .,. Hellman) и Уитфилдом Диффи (Whitfield Diffie) [Diff], и его обобщение - протокол шифрования с открытым ключом Тахира Эль- :-р » * Гамаля (Taher ElGamal). Протокол Диффи - Хеллмана стал прорывом в области криптографии. Это первая в истории криптосистема с открытым ключом, или, иначе, асимметричная криптосистема (см. главу 16). Спустя два года Ривест (Rivest), Шамир (Shamir) и Адлеман (Adleman) опубликовали процедуру RSA (см. работу [Rive]). Сегодня различные разновидности протокола Диффи-Хеллмана используются для распределения ключей в Интернете и в защищенных протоколах безопасности IPSec, IPv6 и SSL, предназначенных для безопасной передачи пакетов на уровне протоколов и для передачи данных на прикладном уровне (например, при электронных платежах). Трудно переоценить практическую значимость этого принципа распределения ключей.2 Ч»'М Два участника протокола Диффи-Хеллмана, назовем их, к примеру, А и В, могут очень просто установить секретный сеансовый ключ, который впоследствии может быть использован для зашифрования передаваемых сообщений. Сначала А и В выбирают большое простое число р и примитивный корень а по модулю р (мы еще вернемся к этому позже). Протокол Диффи-Хеллмана выглядит так. 1 2 Протокол IP Security (IPSec), разработанный Рабочей группой инженеров Internet (Internet Engineering Task Force; IETF), представляет собой всесторонне защищенный протокол и является частью будущего Internert-протокола IPv6. При разработке учитывалась возможность использования этого протокола и в текущем IPv4. Протокол безопасных соединений Secure Socket Layer (SSL) разработан компанией Netscape на основе протокола TCP, обеспечивает оконечный безопасный обмен в приложениях HTTP, FTP и SMTP (обо всем этом см. [Stal], Главы 13 и 14).
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 137 Протокол обмена ключами Диффи-Хеллмана 1. А выбирает произвольное число хь<р- 1 и отправляет В свой открытый ключ уд := а . 2. В выбирает произвольное число хв ^ р - 1 и отправляет А свой открытый ключ уъ '.= а в. 3. А вычисляет секретный ключ sa := у*А mod p. 4. В вычисляет секретный ключ sb := Удв mod p. Поскольку выполняется равенство 5а - УвА = а*вХА ~ У** ~ sb mod Р » • г* после шага 4 у А и у В оказывается один и тот же сеансовый ключ. U- Значения аир, как и значения зд и sb, передаваемые на шагах 1 и 2, ^ могут быть несекретными. Безопасность этого протокола зависит >г от сложности задачи дискретного логарифмирования в конечных :,} полях, а взлом криптосистемы эквивалентен задаче вычисления и значений ха и хв, зная значения уд и ув в Zp.3 Утверждение о том, что вычислить аху, зная ах и </, в конечной циклической группе {задача Диффи-Хеллмана) так же трудно, как вычислить дискретные логарифмы, и, следовательно, об эквивалентности этих задач, ' "А общепринято, но не доказано. »М" i Таким образом, чтобы гарантировать безопасность протокола, Ohi нужно выбирать число р достаточно большим (не менее 1024 бит, 4Т», еще лучше 2048 и больше; см. таблицу 16.1). Кроме того, число /7-1 должно иметь большой простой делитель, близкий к числу (р - 1)/2, чтобы нельзя было применить специальные методы решения задачи дискретного логарифмирования (процедура выбора таких простых чисел будет рассмотрена в главе 16, где мы поговорим о I ~. генерации сильных простых чисел для криптосистемы RSA). Достоинством протокола Диффи-Хеллмана является то, что секретные ключи можно вырабатывать по мере надобности и не нужно хранить в течение длительного времени. Кроме того, для использо- лн вания протокола не требуется никаких дополнительных элементов для согласования параметров а пр. У этого протокола есть и недостатки, самый серьезный из которых - отсутствие доказательства подлинности предаваемых параметров уд и ув. Это делает протокол уязвимым по отношению к «человек посередине», когда нарушитель X перехватывает истинные открытые ключи уд и ув и заменяет их своим поддельным ключом ух. В этом случае А и В вычисляют «секретные» ключи s'A := у£А тос* Р и s'b := Ух* тос* /?, а X, в свою О задаче дискретного логарифмирования см. работы [Schn], п. 11.6 и [Odly].
138 Криптография на Си и C++ в действии очередь, вычисляет ключ s'A ухА* = аХкХх = аХхХк = у** =s'Amodp и, аналогично, ключ s'B. Теперь вместо одного протокола между А ц В получается два протокола: между X и А и между X и В. Таким образом, нарушитель X может перехватить сообщение, переданное •^' *- 1*4" ; участником А, дешифровать его и отправить участнику В поддельное сообщение (то же в обратном направлении). Катастрофа заключается в том, что с точки зрения криптографии А и В даже не будут подозревать о том, что произошло. Для использования в Интернете были предложены различные варианты и обобщения протокола Диффи-Хеллмана, в которых несколько сглажены недостатки и одновременно сохранены достоинства этого протокола. Какова бы ни была версия этого протокола, всегда следует обеспечить возможность проверки подлинности ключевой информации. Это можно сделать, например, так. Участники протокола подписывают цифровой подписью свои открытые ключи и вместе с ключом посылают сертификат, выданный сертифицирующим органом (см. стр. 374, п. 16.3) с использованием протокола SSL. В протоколах IPSec и Ipv6 реализована сложная процедура под названием ISAKMP/Oakley,4 в которой устранены все недостатки протокола Диффи-Хеллмана (подробности см. в работе [Stal], стр. 422-423). Определить примитивный корень по модулю р (иначе - первообразную), то есть такое число а, множество степеней которого a! mod р, i = О, 1, ..., р - 2, совпадает с мультипликативной группой Жр = {1, ...,/?- 1} (см. п. 10.2), можно следующим алгоритмом (см. [Knut], п. 3.2.1.2, теорема С). Предполагается, что известно разложение на простые множители числа р - 1 - порядка мультипликативной группы Жр*: р -1 = р[х... р{к . Вычисление примитивного корня по модулю р 1. Выбрать случайное целое число а из интервала [0, р-1] и положить it— 1. 2. Вычислить t <r- a{p~X)lpi mod p . 3. Если t = 1, то вернуться на шаг 1. Иначе положить i «— i + 1. При i < к вернуться на шаг 2. При i > к результат: а. Реализуем алгоритм в виде следующей функции.
6. Все дороги ведут к... модульному возведению в степень 139 функция: Синтаксис: Вход: Выход: Возврат: Генерация примитивного корня по модулю/? (р > 2 и простое) int primrootj (CLINT aj, unsigned noofprimes, ctint **primesj); noofphmes (число различных простых делителей числа р - 1 - порядка мультипликативной группы) primesj (вектор указателей на CLlNT-объекты, сначала идет/? - 1, а затем простые делители рь ..., рк числа р -1 = р[х... рекк , к = noofprimes) а_1 (первообразный корень по модулю р_1) E_CL1NT_0K, если все в порядке -1, если числор - 1 нечетное и, следовательно, число/? составное ..щи - mi int primrootj (CLINT a J, unsigned int noofprimes, dint *primesj[]) { CLINT pj, tj, junkj; ULONG i; if (ISODD.L (primesj[0])) return -1; } primesj[0] содержит число р - 1, из которого мы получаем модуль в p_l: cpyj (pj, primes_l[0]); incj (pj); SETONE.L (aJ); do incj (aj);
140 Криптография на Си и C++ в действии Искомый примитивный корень а - это натуральное число, большее либо равное 2, поэтому рассматриваем только такие числа. Если а является полным квадратом, то оно не может быть первообразным корнем, поскольку тогда а(р1)/2 = 1 mod p и порядок элемента а меньше, чем ф(р) = р -1. В этом случае увеличиваем значение переменной а_1. Проверка того, является ли а_1 полным квадратом, выполняется с помошью функции issqrJO (см. п. 10.3). •Wi\V if (issqrj (aJ, t_l)) { inc_l (aj); i = 1; (p-D/ft Значение t <r-aKP l),Pi mod/? вычисляем в два этапа. Проверяем все простые делители р; по очереди; используем возведение в степень по Монтгомери. Найденный первообразный корень записывается в а I. do { dlvj (primesj[0], primes_l[i++], tj, junkj); mexpkmj (aj, tj, tj, pj); } while ((i <= noofprimes) && !EQONE_L (tj)); } while (EQONE_L (tj)); return E_CLINT_OK; } Еще одним примером приложения, использующего возведение в степень, является протокол шифрования Эль-Гамаля. Этот протокол является обобщением протокола Диффи-Хеллмана, его стойкость также определяется сложностью задачи дискретного логарифмирования, то есть взлом протокола эквивалентен решению задачи Диффи-Хеллмана (см. стр. 137). С помощью протокола Эль- Гамаля осуществляется управление ключами во всемирно известной
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 141 системе Pretty good privacy (PGP), разработанной Филом Циммерманом (Phil Zimmermann) и используемой для зашифрования и подписи сообщений электронной почты и электронных документов (см. [Stal], п. 12.1). •^ Участник А выбирает открытый и соответствующий секретный ключ следующим образом. Генерация ключей для протокола Эль-Гамаля 1. Участник А выбирает большое простое число р такое, что число /7-1 имеет большой простой делитель, близкий к (р - 1)/2 (см. стр. 363), и примитивный корень а из мультипликативной группы Zp*, как указано выше (см. стр. 138). 2. Участник А выбирает случайное число х такое, что 1 < х < р - 1, и вычисляет b := a mod p с помощью алгоритма Монтгомери. 3. Открытым ключом участника А является тройка {р, а, Ь)А, соответствующим секретным ключом - тройка (р, а, х)А. Теперь участник В с помощью тройки (р, а, Ь)а может зашифровать сообщение Me {1, ..., р- 1} и отправить его участнику А. Протокол выглядит так. i Протокол Эль-Гамаля шифрования с открытым ключом 1. Участник В выбирает случайное число у такое, что 1 < у <р - 1. 2. Участник В вычисляет а := ау mod p и (3 := Mb* mod p = М(ах)у mod p. 3. Участник В отправляет участнику А шифрограмму С := (а, (3). 4. Участник А вычисляет из шифрограммы С открытый текст: М = (3/av mod p. Рассмотренная процедура корректна, поскольку ах (ау)х (ах)у F Значение (3/а* вычисляется как произведение Ра^"1"^ по модулю р. Размер числа р должен быть не менее 1024 бит, в зависимости от приложения (см. таблицу 16.1). Кроме того, при зашифровании двух разных сообщений М{ и М2 следует использовать разные случайные числа ух Ф у2, поскольку в противном случае равенство К _ МФУ _ Мх 32 Мфу ~ М2
142 Криптография на Си и C++ в действии on со позволяет из сообщения М\ вычислить сообщение М2. При практической реализации этого протокола нужно учитывать, что шифрограмма С в два раза длиннее, чем открытый текст М, то есть объем передаваемых данных здесь больше, чем в других протоколах. %%:j Протокол Эль-Гамаля в том виде, как он приведен здесь, обладает весьма любопытным недостатком, благодаря которому нарушитель может получить сведения об открытом тексте, располагая лишь незначительным объемом информации. Циклическая группа Жр* содержит подгруппу U := {ах\ число х четное} порядка (р - 1)/2 (см. [Fisc], глава 1). Если, Ь = ах или а = ау - элемент подгруппы U, то и аху - элемент подгруппы U. Если еще и шифртекст (3 является элементом подгруппы U, то М = $а~ху тоже принадлежит подгруппе U. Аналогичное рассуждение справедливо и в том случае, если ни аху, ни Р не лежат в подгруппе U. В двух оставшихся случаях, - когда в лишь один из элементов аху и Р не лежит в £/, - открытый текст М также не лежит в U. Распознать эту ситуацию позволяет следующий критерий: 1. аху е U <=> (ах е U или ау е U). Эту ситуацию, как и то, лежит ли Р ».8oq ,v< в £/,можно проверить так: 1 2. Для любого ие Жр включение ие U выполняется тогда и только тогда, когда и^~Х)11 = 1. мо« - Насколько же серьезно то, что нарушитель получит такую информацию о сообщении Ml С точки зрения криптографии это совер- " 1' • * f шенно неприемлемо, поскольку в этом случае множество сообщений, по которому ведется перебор, легко сокращается вдвое. На практике допустимость такой ситуации определяется приложением. Отсюда становится понятно, почему не стоит скупиться при выборе }' г ' длины ключа. *-^\ Можно предпринять еще некоторые шаги по устранению указанного недостатка, если не бояться внести новые и неизвестные. На шаге 2 умножение Mb? mod/? можно заменить зашифрованием V(H(axy), M) с помощью подходящего симметричного алгоритма V (это может быть тройной DES, IDEA или новый стандарт шифрования AES; см. главу 19) и хэш-функции Н (см. стр. 373), сжимающей значение аху до размера ключа в алгоритме V. С дом он * 3 Разумеется, это далеко не все приложения, в которых может использоваться модульное возведение в степень. В теории чисел (а значит, ив криптографии) это стандартная операция, и с ней мы еще не раз встретимся в дальнейшем, особенно в главах 10 и 16. Множество прикладных примеров можно найти в работе [Schr] и, конечно, в энциклопедических трудах [Schn] и [MOV].
г Л ABA 7. Поразрядные и логические функции - ... совсем наоборот, - подхватил Труляля. - Если бы это было так, это бы ещё ничего, а если бы ничего, оно бы так и было, но так как это не так, так оно и не этак. Такова логика вещей. |; Льюис Кэрролл, Алиса в Зазеркалье, оЭТ; (Перевод с английского Н. Демуровой) г , В этой главе мы представим функции, реализующие поразрядные г , операции над CLINT-объектами, а также познакомимся поближе с функциями, которыми мы уже отчасти пользовались - для определения равенства и размера CLINT-объектов. К поразрядным функциям относятся и операции сдвига, которые сдвигают CLINT-аргумент в двоичном представлении, изменяя по- : ц зиции отдельных битов, и некоторые другие функции от двух ... CLINT-аргументов, дающие возможность непосредственно работать г.,- с двоичным представлением CLINT-объектов. То, как эти функции можно применить в арифметических целях, наиболее наглядно показывают операции сдвига, описываемые ниже. Мы также видели в '•* п. 4.3, как можно использовать поразрядную операцию AND для ; приведения по модулю, равному степени двойки. 7.1. Операции сдвига Всем движет необходимость. *Ri"; Франсуа Рабле Самый простой способ умножить число а, представленное по осно- >i * :>i ванию В в виде а = (an.\an.2...cio)B, на Ве ~ это сдвинуть а влево на е i^.,;~>;. разрядов. Для двоичного представления это происходит точно так же, как и для хорошо нам известной десятичной системы: е аВ = (дл+е-1а„4<?-2- • -я А-1 • • .«<>)# где ап+е-\ = я„_1, ап+е_2 = Я/1-2» • • •» ае = а0, ае.\ = 0, ..., йо - 0. Для В = 2 это соответствует умножению числа в двоичном пред- е ставлении на 2 , для В = 10 - умножению на степень десяти в десятичной системе.
1 144 Криптография на Си и C++ в действии В аналогичной процедуре для целочисленного деления на степень В разряды числа сдвигаются вправо: - (я„_1.. мп-еап_е-\йп_е_2' • -во)в, Ве ■*шт? где ап_\ = ... = ап-е = 0, ап-е_\ = а„_ь ^п-е-2 - <*п-г, • •> а0 = ае. Для В = 2 это соответствует целочисленному делению числа в дво- е ичном представлении на 2 , для других оснований получается аналогичный результат. Так как разряды CLINT-объектов в памяти представлены в двоичном виде, эти объекты можно легко умножать на степени двойки посредством сдвига налево всех разрядов поочередно, при этом оставшиеся справа пустые разряды заполняются нулями. Аналогичным образом CLINT-объекты можно делить на степени двойки, сдвигая каждый двоичный разряд вправо в сторону младших разрядов. Оставшиеся в конце свободные разряды или заполняются нулями, или игнорируются как ведущие нули. При этом на каждом шаге (сдвиге на один разряд) самый младший разряд теряется. Преимущества этого процесса очевидны. Процедуры умножения и е деления CLINT-объектов на 2 легко выполнимы и требуют не более e["logBfl~| операций сдвига, чтобы переместить каждую величину типа USHORT на один двоичный разряд. Умножение и деление на В использует только ("log^a"] операций для записи USHORT- величин. Далее мы рассмотрим три функции. Функция shl_l() выполняет быстрое умножение CLINT-числа на 2, а функция shr_l() выполняет деление CLINT-числа на 2 и возвращает целое частное. И, наконец, функция shiftJO умножает или делит число а типа е CLINT на 2 . Вид выполняемой операции определяется знаком показателя степени е, который передается функции в качестве аргумента. Если показатель положительный, выполняется умножение, если отрицательный - деление. Если е имеет представление е = В- к + I, I < В, то функция shiftJO выполняет умножение или деление за (/+ 1) I log^a | операций над USHORT-величинами. Все три функции выполняют вычисления над объектами типа CLINT по модулю (Nmax + 1). Они реализованы как функции- сумматоры, то есть изменяют значение своих операндов, записывая в операнд результат вычислений. Функции проверяют наличие переполнения и потери значимости. Однако при сдвигах потери й
ГЛАВА 7. Поразрядные и логические функции 145 значимости не возникает, так как если величина сдвига оказывается больше количества разрядов, то в результате получается нуль. В этом случае значение состояния E_CLINT_UFL для потери значимости просто показывает, что было меньше сдвигов, чем требовалось. Другими словами, степень двойки, выступающая в качестве делителя, оказалась больше, чем делимое, и поэтому частное равняется нулю. Три указанные функции реализованы следующим образом. функция: Синтаксис: Вход: Выход: Возврат: Сдвиг влево (умножение на 2) int shlj (CLINT a J); а_1 (множитель) а_1 (произведение) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения I int shlj (CLINT aj) { dint *ap_l, *msdptraj; ULONG carry = OL; int error =E_CLINT_OK; RMLDZRSJ. (aJ); if (IdJ (aj) >= (USHORT)CLINTMAXBIT) { SETDIGITS_L (aj, CLINTMAXDIGIT); error =E_CLINT_OFL; } msdptraj = MSDPTRJ. (aj); for (apj = LSDPTR_L (aj); apj <= msdptraj; apj++) { *ap_l = (USHORT)(carry = ((ULONG)*ap_l « 1) | (carry » BITPERDGT)); }
146 Криптография на Си и C++ в действии if (carry » BITPERDGT) { if (DIGITS_L (aj) < CLINTMAXDIGIT) { *apj = 1; SETDIGITS.L (aj, DIGITS J- (aj) + 1); error = E_CLINT_OK; } else { error = E_CLINT_OFL; } } RMLDZRSJ. (aj); return error; } Функция: Синтаксис: Вход: Выход: Возврат: Сдвиг вправо (целочисленное деление на 2) int shrj (CLINT aj); aj (делимое) aj (частное) E_CLINT_OK - если все в порядке E_CLINT_UFL - в случае потери значимости int shrj (CLINT aj) { dint *ap_l; USHORT help, carry = 0; if (EQZ_L (aj)) return E_CLINT_UFL; for (apj = MSDPTR_L (aj); apj > aj; apj--)
7. Поразрядные и логические функции 147 { help = (USHORT)((USHORT)(*ap_l » 1) | (USHORT)(carry « (BITPERDGT-1))); carry = (USHORT)(*apJ & 1U); *ap_l = help; } RMLDZRS_L (aj); return E_CLINT_OK; } Сдвиг влево/вправо (умножение/деление на степень двойки) int shiftj (CLINT nj, long Int noofbits); nJ (операнд), noofbits (показатель степени двойки) nj (произведение или частное, в зависимости от знака noofbits) E_CLINT_OK, если все в порядке E_CLINT_UFL в случае потери значимости E_CLINT_OFL в случае переполнения int shiftj (CLINT nj, long int noofbits) { USHORT shorts = (USHORT)((ULONG)(noofbits < 0 ? -noofbits : noofbits) / BITPERDGT); USHORT bits = (USHORT)((ULONG)(noofbits < 0 ? -noofbits : noofbits) % BITPERDGT); long int resl; USHORT i; int error = E_CLINT_OK; dint *nptr_l; dint *msdptrnj; RMLDZRS_L (nj); resl = (int) IdJ (nj) + noofbits; функция: Синтаксис: Вход: Выход: Возврат:
148 Криптография на Си и C++ в действий ] («'■ о Если n_l == 0, нужно лишь правильно установить код ошибки и работа закончена. Аналогично для случая noofbits == 0: if (*nj == 0) return ((resl < 0) ? E_CLINT_UFL : E_CLINT_OK); } if (noofbits == 0) { return E_CLINT_OK; } I Далее проверяется наличие переполнения или потери значимости. Затем, в зависимости от знака noofbits, выбирается сдвиг влево или вправо: if ((res! < 0) || (resl > (long) CLINTMAXBIT)) { error = ((resl < 0) ? E_CLINT_UFL : E_CLINT_OFL); /* Потеря значимости или переполнение?*/ } msdptrnj = MSDPTRJ. (nj); i ) if (noofbits < 0) { Если noofbits < 0, тогда nj делится на 2noofb,ts. Число сдвигаемых разрядов в nj ограничено DIGITS_L (n_l). Сначала сдвигаются целые разряды, а затем оставшиеся биты с помошью shr_l(): shorts = MIN (DIGITS_L (nj), shorts); msdptrnj = MSDPTFLL (nj) - shorts; for (nptrj = LSDPTFLL (nj); nptrj <= msdptrnj; nptrj++)
7. Поразрядные и логические функции 149 { *nptr_l = *(nptr_l + shorts); SETDIGITS_L (nj, DIGITS_L (nj) - (USHORT)shorts); for (i = 0; i < bits; i++) { shrj (nj); } } else { Если noofbits > 0, то n_l умножается на 2noofb,ts. Если число shorts сдвигаемых разрядов больше, чем МАХВ, тогда результат равен нулю. В противном случае сначала определяется и сохраняется число разрядов нового значения и затем сдвигаются целые разряды, а освободившиеся разряды заполняются нулями. Чтобы избежать переполнения, начальная позиция ограничивается n_l + МАХВ и хранится в nptrj. Как и раньше, последние биты сдвигаются по отдельности с помошью shl_l(): 10. if (shorts < CLINTMAXDIGIT) { SETDIGITS_L (nj, MIN (DIGITSJ. (nj) + shorts, CLINTMAXDIGIT)); nptrj = nj + DIGITSJ- (nj); msdptrnj = nj + shorts; while (nptrj > msdptrnj) { *nptrj = *(nptrj - shorts); --nptrj; } while (nptrj > nj) {
1 150 Криптография на Си и C++ в действий *nptrj- = 0; } RMLDZRSJ. (п. for (i = 0; i < bits { shlj (nj); } } else { SETZEROJ. (n. } } return error; J); i++ J); 7.2. Все или ничего: битовые соотношения Пакет FLINT/C содержит функции, позволяющие использовать бинарные С-операторы &, |, Л и для типа CLINT. Однако прежде чем рассмотреть программы, реализующие эти функции, хотелось бы понять, что нам дает их реализация. С математической точки зрения мы рассматриваем соотношения обобщенных булевых функций /: {0,1 р —> {0,1}, которые отображают набор из к чисел (хи •••> хк) е {0, 1}* в 1 или в 0. Действие булевых функций обычно представляется таблицей значений (см. таблицу 7.1). Таблица 7.7. Значения булевых функций *1 0 1 0 1 х2 0 0 1 1 хк 0 0 0 1 Яхъ .../**) 0 1 0 1
ГЛАВА 7; Поразрядные и логические функции 151 В случае битовых соотношений между CLINT-типами сначала будем рассматривать в качестве переменных вектора битов (дгь ...,*„), и формировать значения булевых функций последовательно. Таким образом, мы имеем функции 7:{0,1}ях{0,1}л->{0,1}й, которые отображают и-битовые переменные Х\ :=(jcj, jc2,..., хп) и 2 2 2 х2 := (хр х2,..., лгп) в /г-битовую переменную (хь ..., хп) следующим образом: f(xUX2) :=(/i(*b*2),/2(*b*2). ..../п(^Ь^2)), - 12 гдеД^ьдсг) :=Л*ь*/)- Полученный таким образом вектор (jcb ...,*„) понимается в дальнейшем как число типа CLINT. Решающее значение при выполнении функции / имеет описание частичных функций fh которые определены в терминах булевой функции/. Булевы функции, реализуемые CLINT-функциями and_l(), or_l() и хог_1(), задаются следующим образом (см. таблицы 7.2 - 7.4). Таблица 7,2, Значения CLINT-функиии andJO Таблица 7,3, Значения CLINT-функиии orJO *■■ ммиишшиш Таблица 7,4, Значения CLINT-функиии xorJQ x^ 0 0 1 1 *i 0 0 1 1 Х\ 0 0 1 1 I *2 0 1 0 1 *2 0 1 0 1 *2 0 1 0 1 Яхъх2) 0 0 0 1 ftxux2) 0 1 1 1 Ахьх2) 0 1 1 1
152 Криптография на Си и C++ в лействиц Реализация этих булевых функций тремя С-функциями andjQ orJ() и xorJ() происходит не поразрядно, а посредством обработки разрядов CLINT-переменных стандартными С-операторами &, | и а Каждая из этих функций допускает три аргумента типа CLINT, при» чем первые два являются операндами, а последний - результирую. щей переменной. Функция: Реализация поразрядного AND | Синтаксис: void andj (CLINT aj, CLINT bj, CLINT cj); | Вход: a J, bj (обрабатываемые аргументы) | Выход: cj (результат операции AND) void andj (CLINT aj, CLINT bj, CLINT cj) { CLINT dj; dint *rj, *sj, *tj; dint *lastptrj; i Вначале указатели r_l и s_l устанавливаются на соответствующие разряды аргументов. Если аргументы имеют разное количество разрядов, то s_l указывает на аргумент меньшей длины. Указатель msdptraj - на последний разряд этого аргумента. if (DIGITS J_ (aJ) < DIGITS J_ (bj)) { rj = LSDPTR_L (bj); sj = LSDPTFLL (aj); lastptrj = MSDPTFLL (a J); else { rj = LSDPTFLL (aj); sj = LSDPTRJ. (bj); lastptrj = MSDPTFLL (bj); }
f$A 7. Поразрядные и логические функции 153 Л i ) Теперь указателю t_l ссылается на значение первого разряда результата, а максимальная длина результата хранится в d_l[0]: tj = LSDPTR_L (d_l); SETDIGITS_L (dj, DIGITS.L (sj - 1)); Сама операция выполняется в следующем цикле над разрядами аргумента меньшей длины. При этом результат не может иметь большее количество разрядов. while (sj <= lastptrj) { *t_l++ = *r_l++ & *s_l++; } После того как результат переписывается в c_l (ведушие нули при этом отбрасываются), функция заканчивает работу. cpy_l (c_l, d_l); } Функция: Реализация поразрядного OR Синтаксис: void or_l (CLINT a J, CLINT b_l, CLINT c J); Вход: a_l, bj (обрабатываемые аргументы) Выход: с_1 (результат операции OR) I void or_l (CLINT aj, CLINT bj, CLINT с J) { CLINT dj; dint *r_l, *s_l, *t_l; dint *msdptrr_l; dint *msdptrs_l;
Криптография на Си и C++ в действии Указатели г_1 и s_l задаются так же, как и выше. if (DIGITS J. (aj) < DIGITS_L (bj)) { rj = LSDPTFLL (bj); sj = LSDPTFLL (aJ); msdptrrj = MSDPTRJ. (bj); msdptrsj = MSDPTR_L (aj); } else { rj = LSDPTFLL (aj); sj = LSDPTR_L (bj); msdptrrj = MSDPTR_L (aj); msdptrsj = MSDPTR_L (bj); tj = LSDPTR_L (dj); SETDIGITS_L (dj, DIGITS_L (rj - 1)); Сама операция происходит в цикле над разрядами аргумента меньшей длины: while (sj <= msdptrsj) { *tj++ = *rj++ | *sj++; } Далее берутся остающиеся разряды аргумента большей длины. После того как результат переписывается в c_l (с отбрасыванием начальных нулей), функция завершает работу: while (rj <= msdptrrj)
глАВА 7. Поразрядные и логические функции *t_l++ = *rj++; cpy_l (c_l, d_l); Реализация поразрядного исключающего OR (XOR) void xorj (CLINT a J, CLINT bj, CLINT cj); a J, b_l (обрабатываемые аргументы) с J (результат операции XOR) !i »> №; void xorj (CLINT a J, CLINT bj, CLINT с J) { CLINT dj; dint *rj, *sj, *tj; dint *msdptrrj; dint *msdptrsj; if (DIGITS_L (aj) < DIGITS_L (bj)) { rj = LSDPTR_L (bj); sj = LSDPTR_L (aj); msdptrrj = MSDPTFLL (bj); msdptrsj = MSDPTFLL (aj); } else { rj = LSDPTR_L (aj); sj = LSDPTR_L (bj); msdptrrj = MSDPTR_L (aj); msdptrsj = MSDPTR_L (bj); } tj = LSDPTR_L (dj); SETDIGITSJ. (dj, DIGITS_L (rj - 1));
156 Криптография на Си и C++ в действии () I i Теперь выполняется непосредственно операция. Данный цикл обрабатывает разряды аргумента меньшей длины. while (s_l <= msdptrsj) *t I++ = *r I++ л *s I++; Оставшиеся разряды другого аргумента переписываются, как указано выше: while (r_l <= msdptrrj) *tj++ = *r_l++; } cpy_l (c_l, dj); } Функцию and_l() можно использовать для приведения числа а по модулю степени двойки 2^, задавая CLINT-переменной a_l значение а, CLINT-переменной b_l значение 2* - 1 и вычисляя andj (a_l, b_l, с_1). Однако эту операцию можно выполнить быстрее с помощью созданной для этой цели функции mod2_l(), в которой учитывается тот факт, что двоичное представление числа 2* - 1 состоит исключительно из единиц (см. п. 4.3). 7.3. Прямой доступ к отдельным двоичным разрядам В некоторых случаях оказывается полезной возможность обращаться непосредственно к отдельным двоичным разрядам для того, чтобы прочитать или изменить их. В качестве примера можно упомянуть присваивание CLINT-объекту значения степени 2, которое легко осуществляется заданием значения одного бита. Далее мы подробно рассмотрим три функции, setbit_l(), testbitJO ** clearbitJO, которые соответственно задают, проверяют и удаляют значение отдельного бита. Функции setbit_l() и clearbitJO возвращают состояние указанного бита до выполнения операции. Позиции битов отсчитываются от 0, и заданную позицию можно пред-
7. Поразрядные и логические функции 157 ставить как логарифм степени двойки: если n_l равно 0, то setbitj (n_l, 0) возвращает значение 0 и после выполнения операции n_l о имеет значение 2=1. После обращения к функции setbit_l(n_l, 512) 512 п_1 принимает значение 2 . функция: Проверка и задание значения бита в CLINT-объекте Синтаксис: int setbitj (CLINT aj, unsigned int pos); Вход: a J (C LI NT-аргумент), pos (позиция бита, считая от 0) Выход: а_1 (результат) Возврат: 1 - если значение бита в позиции pos уже было задано 0 - если значение бита в позиции pos не было задано E_CLINT_OFL - в случае переполнения int setbitj (CLINT aj, unsigned int pos) { int res = 0; unsigned int i; —— USHORT shorts = (USHORT)(pos » LDBITPERDGT); USHORT bitpos = (USHORT)(pos & (BITPERDGT- 1)); USHORT m = 1U«bitpos; if (pos > CLINTMAXBIT) return E_CLINT_OFL; } if (shorts >= DIGITSJ- (aj)) { ) I При необходимости a J заполняется нулями пословно, и новая I длина сохраняется в aj[0]: for (i = DIGITS_L (aj) + 1; i <= shorts + 1; i++) { aJ[i] = 0;
158 Криптография на Си и C++ в действии } SETDIGITSJ. (a_l, shorts + 1); } Разряд числа a_l, содержащий позицию указанного бита, проверяется посредством наложения заранее заданной маски т, и после этого бит принимает единичное значение сложением OR соответствуюшего разряда с т. По окончании работы функция возврашает предыдущее значение бита. if (aJ[shorts + 1]&m) { res = 1; } a_J[shorts + 1] |= m; return res; } Функция: Проверка двоичного разряда CLINT-объекта | Синтаксис: int testbitj (CLINT a J, unsigned int pos); | Вход: a J (CLINT-аргумент), pos (позиция бита, считая от 0) | Выход: 1 - если значение бита в позиции pos задано О-в противном случае int testbitj (CLINT a_l, unsigned int pos) { int res = 0; USHORT shorts = (USHORT)(pos » LDBITPERDGT); USHORT bitpos = (USHORT)(pos & (BITPERDGT - 1)); if (shorts < DIGITSJ- (a_l)) { if (aJ[shorts + 1] & (USHORT)(1U « bitpos)) res = 1; }
гддВА 7. Поразрядные и логические функции 159 return res; } функция: Проверка и обнуление бита CLINT-объекта Синтаксис: int clearbitj (CLINT aj, unsigned int pos); Вход: aJ (CLINT-аргумент), pos (позиция бита, считая от 0) Выход: aj (результат) Возврат: 1— если значение бита в позиции pos было задано до удаления О-в противном случае int clearbitj (CLINT aj, unsigned int pos) { int res = 0; USHORT shorts = (USHORT)(pos » LDBITPERDGT); USHORT bitpos = (USHORT)(pos & (BITPERDGT- 1)); USHORT m = 1U«bitpos; if (shorts < DIGITS_L (aj)) { Если a_l имеет достаточное количество разрядов, то разряд числа aj, содержащий позицию указанного бита, проверяется посредством наложения заранее заданной маски m и бит принимает нулевое значение с помошью операции AND соответствуюшего разряда с т. По окончании работы функция возврашает предыдущее значение бита. if (a_l[shorts + 1]&m) { res = 1; } a_l[shorts + 1] &= (USHORT)(~m); RMLDZRS_L (aj); } return res: }
160 Криптография на Си и C++ в действии 7.4. Операции сравнения В каждой программе приходится проверять условия равенств ва/неравенства или соотношения величин арифметических пере* менных. Это требование справедливо и при работе с CLINT- объектами. Здесь мы тоже исходим из того, что программист не обязан знать внутреннюю структуру СLINT-типа, и то, как соотносятся друг с другом два CL!NT-объекта, определяют специально разработанные для этих целей функции. Основная функция, осуществляющая сравнение, - это функция cmp_l(). Она определяет, какое из соотношений выполняется для двух CLINT-величин a_l и b_l - a_l < b_l, a_l == b_l или a_l > b_l. С этой целью сначала сравнивается количество разрядов CLINT- объектов, из которых предварительно исключаются ведущие нули. Если количество разрядов операндов одинаково, тогда работа начинается со сравнения старших разрядов. Как только обнаруживается несовпадение, сравнение заканчивается. Функция: Сравнение двух CLINT-объектов Синтаксис: int crnpj (CLINT a_l, CLINT b_l); Вход: a_l, bj (аргументы) Возврат: -1 - если значение а_1 < значения Ь_ 0 - если значение а_1 = значению Ь_1 1 - если значение а I > значения b I ■..АЗ-»; Щ int crnpj (CLINT aj, CLINT bj) { dint *msdptra_l, *msdptrb_l; int la = DIGITSJ- (aj); int lb = DIGITS_L (bj); Первый тест проверяет, не равны ли нулю длины (а, следовательно, и значения) обоих аргументов. Затем исключаются ведушие нули и делается попытка принять решение, исходя из числа разрядов: if (la == 0 && lb = { return 0; :0)
|-дАВА7. Поразрядные и логические функиии 161 while (a_l[la] == 0 && la > 0) { --la; } while (b_l[lb] == 0 && lb > 0) { --lb; } if (la == 0 && lb == 0) { return 0; } if (la > lb) { return 1; } if (la < lb) { return -1; } Если операнды имеют одинаковое количество разрядов, то необходимо сравнить их значения. Сравнение начинается со старших разрядов и происходит поразрядно вплоть до младших разрядов, пока не обнаруживаются два неравных разряда: msdptraj = a_l + la; msdptrbj = b_l + lb; while ((*msdptraj == *msdptrbj) && (msdptraj > a_l)) { msdptraj-; msdptrbj--; }
162 Криптография на Си и C++ в действии d ) Теперь сравниваем эти два разряда и делаем вывод. Функция при этом возврашает соответствующее значение: if (msdptraj == a_l) { return 0; } if (*msdptra_l > *msdptrb_l) { return 1; } else { return-1; } } Если нас интересует равенство двух CLINT-величин, то применение функции стр_1() влечет за собой выполнение излишних операций. Для этого случая имеется более простой ее вариант, где отсутствует сравнение размеров. Функция: Синтаксис: Вход: Возврат: Сравнение двух CLINT-объектов int equj (CLINT a J, CLINT bj); a J, b_l (аргументы) 0 - если значение aj Ф значению b_l 1— если значение а_1 = значению Ь_1 J int equj (CLINT a J, CLINT bj) dint *msdptra_l, *msdptrb_l; int la = DIGITSJ. (aj); int lb = DIGITS_L (bj); if (la == 0 && lb == 0)
т гдДВА 7. Поразрядные и логические функции 1 £3 /.н ■•' г г ■ он*:? „. ;f*q^ - Lfi *:1, { return 1; } while (a_l[la] == 0 && la > 0) { --la; } while (b_l[lb] == 0 && lb > 0) { --lb; } if (la == 0 && lb == 0) { return 1; } if (la != lb) { return 0; } msdptraj = aj + la; msdptrbj = bj + lb; while ((*msdptraj == *msdptrbj) && (msdptraj > aj)) { msdptraj-; msdptrbj--; } return (msdptraj > a J ? 0 : 1); }
164 Криптография на Си и C++ в действий Применение пользователем этих двух функций в непосредственном виде легко приведет к многочисленным ошибкам. В частности, смысл результатов функции cmp_l() необходимо твердо запомнить, или же придется их периодически освежать в памяти. В качестве средства против ошибок было разработано множество макросов, с помощью которых можно представить условия сравнения в более удобном виде. (см. Приложение С, «Макросы с параметрами»). Например, есть следующие макросы, в которых объекты a_l и bj приравниваются к их величинам: GEJ_ (a_l, bj) возвращает 1, если а_1 >= Ь, и 0 - в противном случае; EQZ_L (aj) возвращает 1, если а_1 == 0, и 0, если а_1 > 0. || I
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования Теперь числа из двоичной системы в десятичную преобразовывались автоматически... 881, 883, 887, 907... знакомые простые числа. } Карл Саган, Контакт mmi В начале этой главы рассмотрим самую простую и одновременно самую важную функцию - присваивание. Для того чтобы присвоить CLINT-объекту а_1 значение другого CLINT-объекта Ь_1, нам нужна функция, которая копирует разряды bj в область памяти, отведенную под а_1, то есть выполняет действие, которое мы будем называть поэлементным присваиванием. Нельзя обойтись простым копированием адреса объекта bj в переменную а_1, так как в этом случае оба объекта будут ссылаться на одну и ту же ячейку памяти, а именно на ячейку bj, и любое изменение в aj будет отражаться л на объекте bj и наоборот. К тому же доступ к области памяти, на которую ссылается а_1, может оказаться потерянным. Мы вернемся к проблеме поэлементного присваивания во второй части этой книги, когда коснемся вопроса реализации оператора присваивания «=» в языке C++ (см. п. 13.3). Присваивание значения одного CLINT-объекта другому выполняется функцией сру_1(): Копирование CLINT-объекта как операция присваивания void cpyj (CLINT destj, CLINT srcj); srcj (присваиваемое значение) destj (объект назначения) void cpyj (CLINT destj, CLINT srcj) { dint *lastsrcj = MSDPTFLL (srcj); *destj = *srcj; На следующем шаге находятся ведушие нули, которые потом отбрасываются. Одновременно устанавливается количество разрядов в выходном объекте. Функция: Синтаксис: Вход: Выход: ( ') Ч, (
166 Криптография на Си и C++ в действии while (('lastsrcj ~ 0) && (*destj > 0)) { --lastsrcj; ~*destj; } Теперь значимые разряды исходного объекта копируются в объект назначения. После этого функция заканчивает работу: while (src_l < lastsrcj) { ••'■■'" ■• ' : *++dest_l = *++srcJ; } .-■ .- .,;;.■■ •- ,, Обмен значениями двух С LI NT-объектов осуществляется с помощью макроса SWAP_L (FLINT/C-варианта макроса SWAP), который весьма интересным способом выполняет эту задачу посредством операций XOR, не вводя временные переменные для промежуточного хранения: •%■* #define SWAP(a, b) ((а)Л=(Ь), (Ь)Л=(а), (а)Л=(Ь)) #define SWAP_L(a_l, bj) \ (xorJ((aJ), (bj), (aj)),\ xorJ((bJ), (aj), (b_l)),\ xorJ((a_l), (bj), (a_l))) Недостатком этих макросов является то, что если их входные аргументы являются некоторыми выражениями, то могут возникать побочные эффекты их повторного вычисления и отсюда труднооб- наружимые ошибки. Для SWAP_L это не так критично, так как эта функция может работать только с указателями на CLINT-объекты, и любое выражение при вызове этой функции все равно должно быть преобразовано в такой указатель. В необходимых случаях вместо макроса можно использовать функцию fswap_l(). I Функция: Перестановка значений двух CLINT-объектов | Синтаксис: void fswapj (CLINT a J, CLINT bj); | Вход: .,:.„,-, aj, bj (переставляемые значения) Г | Выход: a J, bj
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 167 Хотя функции библиотеки FLINT/C для ввода и вывода чисел в доступном для понимания виде не принадлежат к числу поражающих воображение, все же во многих приложениях без них не обойтись. Из практических соображений вид этих функций выбирался таким образом, чтобы ввод и вывод осуществлялся в виде строк символов (векторов типа char). Для этого были разработаны две по существу дополняющие друг друга функции str2clint_J() и xclint2str_l(): первая преобразует строку цифр в CLINT-объект, а вторая, наоборот, преобразует CLINT-объект в строку. При этом задается основание для представления строк, причем допускаются представления по основанию в интервале от 2 до 16. Функция str2clint_l() осуществляет преобразование представления в объект типа CLINT из представления по заданному основанию. Такое преобразование выполняется посредством последовательных умножений и сложений по основанию В (см. [Knut], п. 4.4). Функция отмечает все случаи переполнений, использования недопустимого основания и передачи пустых указателей и возвращает соответствующие коды ошибок. Любые префиксы, характеризующие представление числа - «ОХ», «Ох», «ОВ» или «Ob» игнорируются: Функция: Преобразование строки в CLINT-объект Синтаксис: int str2clint_l (CLINT n_l, char *str, USHORT base); Вход: str (указатель на последовательность типа char) base (основание числового представления строки, 2 < base < 16) Выход: п_1 (выходной CLINT-объект) Возврат: E_CLINT_OK, если все в порядке E_CLINT_BOR, если base < 2 или base > 16, или в str есть разряды, большие, чем base E_CLINT_OFL в случае переполнения E_CLINT_NPT, если в str передан пустой указатель int str2clint_l (CLINT n_l, char *str, USHORT base) { dint base_l[10]; ; { ' . :*' " " '; \ USHORT n; , \\.\. . -i' | int error = E_CLINT_OK; • r - H | if (str == NULL) , {
168 Криптография на Си и С++ в действии return E__CLINT_NPT; } О Q- if (2 > base || base > 16) { return E_CLINT_BOR; Г Ошибка: недопустимое основание */ } u2clintj (basej, base); SETZEROJ. (nj); г эеа с : щищ 4i ^ Г w if (*str == '0') { if((to!owerJ(*(str+1))==,x')|| (tolower_l(*(str+1)) == 'b')) I* Игнорировать любой префикс */ { ++str; ++str; } } while (isxdigit ((int)*str) || isspace ((int)*str)) { if (!isspace ((int)*str)) { n = (USHORT)tolowerJ (*str); Многие реализации функции tolowerO из С-библиотек, несовместимых с ANSI, возврашают неопределенные результаты, если символ не является заглавным. FLINT/C-функиия tolowerJO обращается к tolowerO только в случае заглавных A-Z, в противном случае возврашает символ неизмененным.
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 169 switch (n) { case 'a': case Ъ': case 'с': case 'сГ: case 'е': case 'f: n-=(USHORT)('a'-10); break; default: n -= (USHORT)'O'; } if (n >= base) { error = E_CLINT_BOR; break; } if ((error = mulj (nj, basej, nj)) != E^CLINT_OK) { break; } if ((error = uaddj (nJ, n, nj)) != E_CLINT_OK) { break; } } ++str; } return error;
1 170 Криптография на Си и C++ в действии Функция xclint2str_l(), обратная к функции str2clintj(), возвращает указатель на внутренний буфер класса памяти static (см. [Harb], п. 4.3), который сохраняет полученное численное представление и его значение до следующего вызова функции xclint2str() или до окончания работы программы. Функция xclint2str_J() выполняет требуемое преобразование CLINT- представления в представление по заданному основанию путем последовательных делений с остатком на основание В. Функция: Синтаксис: Вход: Возврат: Преобразование CLINT-объекта в строку символов char * xclint2str_l (CLINT nj, USHORT base, int showbase); nj (преобразуемый CLINT-объект) base (основание численного представления искомой строки) showbase (значение Ф 0: численное представление имеет префикс «Ох» для base = 16 или «0Ь» для base = 2. Значение = 0: префикса нет). указатель на полученную строку символов, если все в порядке NULL - если 2 < base или base > 16 static char ntable[16] = {Ю717273747576\778797а7Ь7с7й7е',Т}; char* xclint2str_l (CLINT nj, USHORT base, int showbase) { CLINTD u_l, rj; dint base_l[10]; int i = 0; static char N[CLINTMAXBIT + 3]; i if (2U > base || base >16U) { return (char *)NULL; /* Ошибка: недопустимое основание */ } u2clint_l (basej, base); cpy_l (u_l, nj);
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 171 do { (void) divj (uj, basej, uj, rj); if (GTZ_L (rj)) { N[i++] = (char) ntable[*LSDPTR_L (rj) & Oxff]; } i/ else 'иЧ f N[i++] = '0'; } } while (GTZ_L (uj)); if (showbase) { switch (base) A'. { il* case 2: N[i++] = 'b'; ; N[i++] = '0'; break; ™~**^..,^. case 8: N[i++] = '0'; break; case 16: N[i++] = *x'; N[i++] = '0'; break; } } N[i] = '\0*;
172 ^лмррицм. uni i j. . Криптография на Си и C++ в действии return strrevj (N); П Для совместимости с функцией clint2str_l() в первом издании книги clint2str_l (n_l, base) описывается как макрос, вызывающий функцию xclint2str(nj, base, 0). Кроме того, были разработаны макросы HEXSTR_L(), DECSTR_L(), OCTSTR_L() и BINSTR_L(), которые, используя в качестве аргумента переданный CLINT-объект, создают строку символов без префикса с численным представлением, заданным именем макроса. Таким образом, основание представления исключается из числа аргументов (см. приложение С). В качестве стандартной формы для вывода CLINT-значений мы располагаем готовым макросом DISP_L(), аргументами которого являются указатель на строку символов и CLINT-объект. Строка символов содержит, в зависимости от своего назначения, информацию о выводимом далее CLINT-значении, например, такую: «Произведение aj и bj имеет значение...». CLINT-значение выводится в шестнадцатиричном формате, то есть по основанию 16. Дополнительно DISP_L() выводит с новой строки количество значимых двоичных разрядов (без учета начальных нулей) указанного CLINT- объекта (см. приложение С). В случае преобразования друг в друга байтовых векторов и CLINT- объектов можно применить пару функций byte2clint_l() и clint2byte_l() (см. [IEEE], 5.5.1). Предполагается, что байтовые вектора осуществляют численное представление по основанию 256 с возрастающими справа налево значениями. Подробную реализацию этих функций читатель найдет в файле flint.c. Здесь мы приводим только их заголовки. Функция: Преобразование байтового вектора в CLINT-объект Синтаксис: int byte2clint_l (CLINT n_l, UCHAR "bytestr, int len); I Вход: bytestr (указатель на последовательность типа UCHAR) len (длина байтового вектора) I Выход: п_1 (выходной CLINT-объект) | Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения E_CLINTJ\IPT, если в bytestr был передан пустой указатель
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 173 функция: Преобразование CLINT-объекта в байтовый вектор Синтаксис: UCHAR * clint2bytej (CLINT nj, int *len); Вход: nj (преобразуемый CLINT-объект) Выход: len (длина генерируемого байтового вектора) Возврат: указатель на полученный байтовый вектор NULL, если в len передан пустой указатель И наконец, для перевода значений типа unsigned в численный формат CLINT можно использовать две функции: u2clint_l() и ul2clintj(). Функция u2clintj() преобразует аргументы типа USHORT (а функция ul2cllntj() - аргументы типа ULONG) в численный формат CLINT. Например, функция u2clintj() в дальнейшем будет описываться следующим образом: Функция: Преобразование значения типа ULONG в CLINT-объект Синтаксис: void ul2clintj (CLINT numj, ULONG ul); Вход: ul (преобразуемое значение) Выход: numj (выходной CLINT-объект) I void ul2clintj (CLINT numj, ULONG ul) { ♦LSDPTRJ. (numj) = (USHORT)(ul & Oxffff); *(LSDPTR_L (numj) + 1) = (USHORT)((ul » 16) & Oxffff); SETDIGITSJ. (numj, 2); RMLDZRS_L (numj); } Завершая эту главу, рассмотрим функцию, которая выполняет проверку правильности числового формата CLINT-объекта. Контрольные функции этого типа вызываются всякий раз, когда в систему вводятся «чужеродные» величины для дальнейшей обработки в подсистеме. Такой подсистемой может оказаться, например, какой- либо криптографический модуль, который перед каждой обработкой входных данных должен проверить их допустимость. Динамическая проверка входных значений функции весьма полезна в практике программирования, так как она позволяет избегать неопределённых ситуаций и имеет решающее значение для стабильной работы приложений. При тестировании и отладке эта проверка обычно происходит с помощью утверждений (assertions), проверяющих условия
174 Криптография на Си и C++ в действии Ifi?,: во время выполнения. Утверждения вставляются в программу как макросы, и при фактическом выполнении программы, обычно во время компиляции, их можно отключить посредством #define NDEBUG. В дополнение к макросу assert стандартной библиотеки языка С (см. [Plal], глава 1) имеется ещё несколько подобных реализаций, которые выполняют различные действия в случае жестких условий тестирования, например, такие как распечатка обнаруженных исключительных ситуаций в файл регистрации, с завершением (или без завершения) работы программы при появлении ошибки. Подробную информацию по этому вопросу читатель может найти в [Magu], главы 2 и 3, а также в [Murp], глава 4. Защита функций из библиотеки программ (такой как пакет FLINT/C) от передачи значений, не входящих в область определения соответствующих параметров, может происходить внутри или самих вызываемых функций, или функций, их вызывающих. В последнем слу- , чае вся ответственность возлагается на программиста, пользующе- *>„w~ гося этой библиотекой. Исходя из соображений эффективности, )^((Tjc при разработке FLINT/C-функций мы не проверяли каждый передаваемый CLINT-аргумент на правильность адреса и на возможность переполнения. Представив многократные проверки формата числа для тысяч операций модульного умножения или возведения в степень, автор решил переложить эту задачу на сами программы, использующие функции из FLINT/C. Исключением является пере- ^'~" дача делителей с нулевым значением. Здесь проверка имеет принципиальное значение, и обнаружение нуля подтверждается сообщением о соответствующей ошибке во всех функциях, реализующих арифметику классов вычетов. Текст всех этих функций проверялся особенно тщательно, чтобы удостовериться, что библиотека FLINT/C генерирует только допустимые форматы (см. главу 12). Для проверки правильности формата CLINT-аргументов была разработана функция vcheck_l(). Она предназначена для защиты FLINT/C-функций от передачи недопустимых параметров в качестве CLINT-величин. Функция: Синтаксис: Вход: Возврат: Проверка правильности числового формата CLINT-объекта intvcheckj (CLINT nj); nj (проверяемый объект) E_VCHECK_OK - если формат правильный сообщение об ошибке в соответствии с таблицей 8.1 1 int vcheckj (CLINT nj) { unsigned int error = E_VCHECK_OK; I
ГЛАВА 8. Операиии ввода, вывода, присваивания и преобразования 175 Проверяем наличие нулевого указателя: самая ужасная из ошибок. if (nj == NULL) { error = E_VCHECK_MEM; } else f j I Проверяем наличие переполнения: не слишком ли много у числа I разрядов? if (((unsigned int) DIGITS.L (nj)) > CLINTMAXDIGIT) { error = E_VCHECK_OFL; } else { _ j Проверяем наличие ведущих нулей: с ними можно жить. | я _ — J if ((DIGITS_L (nj) > 0) && (n_l[DIGITS_L (nj)] == 0)) { error = E_VCHECK_LDZ; } } } return error;
176 Криптография на Си и C++ в действии Возвращаемые функцией значения описаны как макросы в файле flint.c. Их объяснение приводится в таблице 8.1. Таблииа 8.7. * Сообщения функиии vcheckJO о результатах проверки Возвращаемое значение E_VCHECK_OK E_VCHECK_LDZ E_VCHECK_MEM E_VCHECK_OFL Сообщение Format is OK leading zeros memory error genuine overflow Интерпретация Число имеет допустимое представление, и его значение лежит в пределах CLINT-типа. Предупреждение: число обладает ведущими нулями, однако его величина лежит в допустимых пределах. Ошибка: передан нулевой указатель Ошибка: переданное число слишком большое; его нельзя представить как CLINT-объект. • «* *йздищ» «ьшътшь* - „лГШЖ ш >>'m>mm?mmm«,.»мг «*чии Численные значения кодов ошибок меньше нуля, поэтому достаточно простого сравнения с нулем, чтобы отличить ошибки от предупреждений (или от допустимого случая). u;l^
ГЛАВА 9. Динамические регистры - От глупости этой машины я впадаю в депрессию, - сказал Марвин и поковылял прочь. Дуглас Адаме, Ресторан на краю Вселенной. Кроме автоматических или, в исключительных случаях, глобальных CLINT-объектов, используемых до сих пор, иногда оказывается полезным умение создавать и уничтожать CLINT-переменные автоматически. С этой целью разработаем несколько функций, которые позволят нам генерировать, использовать, удалять и перемещать совокупность CLINT-объектов - так называемый банк регистров - как динамически распределенную структуру данных. Мы воспользуемся схемой, представленной в [Skal], и приспособим её для работы с CL1 NT-объектами. Будем разделять эти функции на закрытые функции управления и открытые функции. Последние будут доступны другим внешним функциям для работы с регистрами. Однако сами CLINT-функции не используют эти регистры, так что полный контроль над использованием регистров могут обеспечить функции пользователя. При выполнении программы должно быть установлено число доступных регистров. Для этого нам требуется статическая переменная NoofRegs, принимающая значение числа регистров, которое определяется встроенной константой NOOFREGS. static USHORT NoofRegs = NOOFREGS; Теперь определим центральную структуру данных для управления банком регистров: struct clint_registers { int noofregs; int created; dint **reg_l; /* указатель на вектор CLINT-адресов */ Структура clint_registers содержит: переменную noofregs, которая описывает число регистров, помещенных в наш банк; переменную created, которая указывает, выделена ли эта совокупность регистров; указатель regj на вектор, содержащий начальные адреса отдельных регистров. ii static struct clint_registers registers = {0, 0, 0};
178 Криптография на Си и C++ в действии Теперь рассмотрим закрытые функции управления: allocate_regJ() „ чтобы создать банк регистров, и destroy_reg_l() - чтобы уничтожить его. Сначала создается область хранения адресов выделяемых регистров и устанавливается указатель на переменную registers.regj. После этого выделяется память для каждого отдельного регистра посредством вызова mallocQ из стандартной библиотеки языка С. Тот факт, что CLINT-регистры являются единицами памяти, выде- ** ' ленными с помощью mallocQ, играет важную роль при тестировании FLINT/C-функций. В п. 12.2 мы увидим, что благодаря этому можно проверять память на наличие любых возможных ошибок. и ( | ггто Prtt*i/f;. static int allocate_reg_l (void) { USHORT i, j; i Сначала выделяем память для вектора с адресами регистров. if ((registers.regj = (dint **) malloc (sizeof(clint *) * -a«>j; <* NoofRegs)) == NULL) return E_CLINT_MAL; } Теперь выделяем отдельные регистры. Если в процессе работы вызов mallocO завершится ошибкой, все регистры, выделенные до этого, очишаются и возвращается код ошибки E_CLINT_MAL: V £Ю..:\>\, for (i = 0; i < NoofRegs; i++) { if ((registers.reg_l[i] = (dint *) malloc (CLINTMAXBYTE)) == NULL) { for(j = 0;j<i;j++) { free (registers.regj[j]); } return E__CLINT_MAL; /* ошибка: malloc */ }
ГЛАВА 9. Динамические регистры 179 } return E_CLINT_OK; } Функция destroy_regJ() по существу является противоположной функции create_reg_l(). Сначала обнуляется содержимое регистров. Затем память, выделенная под каждый регистр, возвращается с помощью free(). Наконец, освобождается область памяти, на которую указывал registers.reg_l. static void destroy_reg_l (void) { USHORT i; Г4 for (i = 0; i < registers.noofregs; i++) { memset (registers.reg_l[i], 0, CLINTMAXBYTE); free (registers.reg_l[i]); } free (registers.regj); } Теперь рассмотрим общие функции для управления регистрами. С помощью функции create_reg_J() образуем совокупность отдельных регистров, число которых определяется константой NoofRegs. Это осуществляется с помощью обращения к закрытой функции allocate_reg_l(). Функция: Синтаксис: Возврат: Выделение совокупности регистров CLINT-типа int create_regJ (void); E_CLINT_OK - если все в порядке E_CLINT_MAL - в случае ошибки, связанной с malloc() int create_regj (void) { int error = E CLINT OK;
180 Криптография на Си и C++ в лей* if (registers.created == 0) { error = allocate_reg_l (); registers.noofregs = NoofRegs; } if (lerror) { ++registers.created; } return error; } Структура registers включает в себя переменную registers.created, которая используется для подсчета числа требуемых регистров. В результате вызова функции free_reg_l(), описанной ниже, получаем совокупность регистров, которые освобождаются только в том случае, если значение registers.created равно 1. В противном случае registers.created просто уменьшается на 1. Используя этот механизм, называемый семафором, мы предотвращаем ситуацию, когда совокупность регистров, выделенных одной функцией, может быть без всякого указания освобождена другой функцией. С другой стороны, каждая функция, которая запрашивает совокупность регистров, вызывая create_reg_l(), должна освобождать их посредством опять же free_reg_l(). Более того, нельзя допускать, чтобы после вызова функции регистры содержали конкретные значения. Переменную NoofRegs, определяющую число регистров, создаваемых функцией create_regj(), можно изменять посредством функции set_noofregs_l(). Однако это изменение остаётся действительным только до тех пор, пока не будет освобождена текущая выделенная совокупность регистров и создана новая с помощью create_regj(). Функция: Задание числа регистров I Синтаксис: void set_noofregs_l (unsigned int nregs); I Вход: nregs (число регистров в банке регистров) ,r J void set_noofregsJ (unsigned int nregs) {
I гдАВ^Ш- Динамические регистры 181 NoofRegs = (USHORT)nregs; } Теперь, когда мы умеем выделять совокупность регистров, можно задаться вопросом, как получить доступ к отдельным регистрам. Для этого необходимо выбрать динамически выделенное функцией create_reg_l() поле адреса regj в описанной выше структуре clint_registers. Эта задача выполняется посредством функции get_reg_J(), представленной ниже, которая возвращает указатель на отдельный регистр из совокупности, при условии, что выделенный регистр обозначается определенным порядковым числом. функция: Синтаксис: Вход: Возврат: Вывод указателя на регистр dint * get_reg_l (unsigned int reg); reg (номер регистра) указатель на требуемый регистр reg, если он выделен NULL, если регистр не выделен dint * get_reg_l (unsigned int reg) { if (Iregisters.created || (reg >= registers.noofregs)) { return (dint *)NULL; } return registers.reg_l[reg]; } Так как размер совокупности регистров и её расположение в памяти может измениться, то не рекомендуется сохранять уже считанные адреса регистров для использования их в дальнейшем. Намного предпочтительнее каждый раз заново получать адреса регистров. В файле flint.c можно найти несколько встроенных макросов вида #define rO_l get_reg_l(0); С помощью этих макросов можно вызывать регистры по их фактическим текущим адресам, не прибегая к дополнениям в тексте программы. Используя функцию purge_reg_l(), можно очистить отдельный регистр совокупности путём затирания его содержимого. ».г*. *■ О >!';
182 Криптография на Си и C++ в действии I функция: Очистка CLINT-регистра из банка регистров с помощью заполнения его нулями | Синтаксис: int purge_reg_l (unsigned int reg); | Вход: reg (номер регистра) | Возврат: E_CLINT_OK, если все в порядке E_CLINT_NOR, если регистр не выделен int purge_reg_l (unsigned int reg) { if (Iregisters.created || (reg >= registers.noofregs)) { return E_CLINT_NOR; } memset (registers.reg_l[reg], 0, CLINTMAXBYTE); return E_CLINT_OK; } Подобным же образом с помощью функции purgeall_reg_l() можно очистить всю совокупность регистров. Функция: Очистка всех CLINT-регистров с помощью записи в них нулей | Синтаксис: int purgeall_regj (void); [ Возврат: E_CLINT_OK, если все в порядке E_CLINT_NOR, если регистры не выделены int purgeall_reg_l (void) { USHORT i; if (registers.created) { for (i = 0; i < registers.noofregs; i++) memset (registers.regjfl], 0, CLINTMAXBYTE); }
r-ддВА 9. Динамические регистры 183 return E_CLINT_OK; } - ilOO 07 ОЧ'л return E_CLINT_NOR; } В программировании считается хорошим стилем и проявлением профессиональной этики освобождать выделенную память, когда она больше не нужна. Имеющуюся в наличии совокупность регистров можно освободить с помощью функции free__reg_l(). Однако, как мы уже объясняли выше, перед освобождением выделенной памяти семафор registers.created в структуре registers должен быть установлен в 1: void free_regj (void) { if (registers.created == 1) { destroy_reg_l (); } if (registers.created) { -registers.created; } } Теперь рассмотрим три функции, которые создают, очищают и освобождают отдельные CLINT-регистры, по аналогии с управлением всей совокупностью регистров. Функция: Выделение регистра CLINT-типа Синтаксис: dint * createj (void); Возврат: указатель на выделенный регистр, если все в порядке NULL в случае ошибки, связанной с malloc() dint * createj (void) {
184 Криптография на Си и C++ в действии иш return (dint *) malloc (CLINTMAXBYTE); } При этом важно не «потерять» возвращаемый функцией createjQ указатель, так как иначе будет невозможно получить доступ к соз- данному регистру. Последовательность dint * do_not_overwrite_l; dint * lostj; Г ... 7 do_not_overwrite_l = create_l(); Л ... */ do_not_overwrite_l = lostj; i выделяет регистр и хранит его адрес в переменной с соответствующим именем do_not_overwriteJ (не затирать). Если эта переменная содержит только ссылку на регистр, то после выполнения команды do_not_overwrite_l = lostj; регистр оказывается висячим (или потерянным, на него больше ничто не ссылается). Это типичная ошибка, которую можно допустить, блуждая «в дебрях» управления динамической памятью. Регистр, как и любую другую CLINT-переменную, можно очистить с помощью представленной ниже функции purgeJO, посредством которой зарезервированная для указанного регистра память заполняется нулями и таким образом очищается. IФункция: Очистка CLINT-объекта с помощью заполнения нулями Синтаксис: void purge J (CLINT nj); Вход: nj (CLINT-объект) void purgej (CLINT nj) { if (NULL != nj) memset (nj, 0, CLINTMAXBYTE); } i
ГЛАВА 9. Динамические регистры 185 Следующая функция после очистки регистра к тому же освобождает выделенную для него память. После этого к регистру больше нельзя получить доступ. функция: Очистка и освобождение СLINT-регистра Синтаксис: void freej (CLINT regj); Вход: regj (указатель на CLINT-регистр) void freej (CLINT regj) { if (NULL != regj) { memset (regj, 0, CLINTMAXBYTE); free (nj); } }
rjlABA 10. Основные теоретико-числовые функции Я ужасно хочу это услышать, поскольку я всегда считал теорию чисел Королевой Математики - самым чистым направлением математики - единственным направлением, НЕ имеющим прикладного значения! Д.Р. Хофштадтер, Гедель, Эшер, Бах Вооруженные целым арсеналом арифметических функций, разра- ^ ботанных в предыдущих главах, обратимся теперь к реализации не- кн которых фундаментальных алгоритмов из теории чисел. Коллекция -*г теоретико-числовых функций, которые нам предстоит обсудить, с одной стороны, позволяет уяснить прикладные аспекты арифметики J" больших чисел, а с другой стороны, является этапом на пути к более "-• i сложным теоретико-числовым вычислениям и криптографическим к- приложениям. Рассматриваемые функции допускают сколь угодно широкое обобщение, так что из них можно «собрать» вычислительный модуль почти для любого приложения. Алгоритмы, на основе которых написаны программы, приведенные в этой главе, взяты в основном из работ [Cohe], [HKW], [Knut], п! [Kran] и [Rose]. Как и раньше, нас особенно интересует эффек- го, тивность и как можно более широкий спектр действия этих алгоритмов. |п Что касается математической теории, то здесь мы приведем лишь l1^ тот минимум, который необходим для уяснения представленных функций и обоснования возможности их применения, - в конце концов, можно и отдохнуть. Те же, кто ожидал более радикального введения в теорию чисел, смогут найти его в книгах [Bund] и [Rose]. Алгоритмические аспекты теории чисел ясно и кратко изложены в работе [Cohe]. Информативный обзор теоретико- числовых приложений дан в [Schr], а криптографические аспекты теории чисел - в [КоЫ]. В этой главе, помимо всего прочего, мы рассмотрим способы вычисления наибольшего общего делителя и наименьшего общего кратного больших чисел, мультипликативные свойства кольца классов вычетов, научимся распознавать квадратичные вычеты и извлекать квадратные корни в кольцах классов вычетов, научимся применять китайскую теорему об остатках для решения систем линейных сравнений и тестировать числа на простоту. Теоретические сведения будут подкреплены практическими примерами. Кроме того, мы разработаем несколько функций, реализующих описанные алгоритмы, и укажем приложения, в которых эти функции могут быть полезны. I
188 Криптография на Си и C++ в действии ШЯтшш шттттттшшит^шттштттштш in"" ''аШ'!-'''' ' тштщШЯтщ»шттт ш^МШ^Ш. Д ЩЩМШШ Щ 10.1. Наибольший обший делитель То, что школьников учат использовать для вьь числения наибольшего общего делителя двух целых чисел метод разложения на простые множители, а не более естественный способ -. алгоритм Евклида, - позор для нашей системы образования. У. Хейс, П. Кватроччи, Информация и теория кодирования, 19831 Наибольшим общим делителем (НОД) целых чисел а и b называется положительный делитель чисел а и Ь, делящийся на любой другой общий делитель этих чисел. Таким образом, наибольший общий делитель определен однозначно. В математических обозначениях число d является наибольшим общим делителем двух ненулевых целых чисел а и b, d= НОД(я, Ь), если d > 0, d \ я, d \ b и если для некоторого целого d! выполняются условия d! \ а и d! \ Ъ, <w то йГ | d. Это определение удобно дополнить, введя в рассмотрение случай НОД(0, 0) := 0. f Таким образом, мы определили наибольший общий делитель для -: всех пар целых чисел, в частности, для всех чисел, представимых CLINT-объектами. Выполняются следующие свойства: ь« м (а) НОД(а, Ь) = НОД(Ь, а), (10.1) (б) НОД(а, 0) = \а\ (абсолютное значение числа а), (в) НОД(а, Ъ% с) = НОД(л, НОД(/?, с)), (г) НОД(а, Ь) = НОД(-£1, 6), из которых лишь первые три применимы к CLINT-объектам. Сначала непременно следует рассмотреть классический алгоритм вычисления наибольшего общего делителя, которым мы обязаны греческому математику Евклиду (III в. до н. э.) и который Д. Кнут уважительно называет «дедушкой» всех алгоритмов (см. [Knut], стр. 316 и далее). В основе алгоритма Евклида лежит повторное деление с остатком: вычисляется вычет a mod /?, затем b mod (a mod b) и так далее, пока остаток не будет равен нулю. Что удивительно, в России то же самое. - Прим. перев.
ГЛАВА 10. Основные теоретико-числовые функции 189 Алгоритм Евклида вычисления НОД(а, Ь) для a, b > 0 (К; 1. Если b = 0, то результат: а и алгоритм заканчивает работу. 2. Положить г <— a mod /?, я <— Z?, Z? <— г и вернуться на шаг 1. NT. •■по: '' т,- Г- Для натуральных чисел аи аг процесс вычисления наибольшего общего делителя по алгоритму Евклида выглядит так: ах = a2q\ + а3, 0 < я3 < аъ <*2 = аъЧг + а*, 0 < я4 < #з> «3 = «4^3 + «5» 0 < Я5 < «4» Я/п-2 = «m-l<7w-2 + Я/п. 0 < dm < Ят_Ь Результат: НОД(аь а2) = ат. В качестве примера вычислим НОД(723, 288): 723 = 288 = 147 = 141 = 6 = = 288- 2+147, = 147 • 1 + 141, = 141- 1+6, = 6- 23 + 3, = 3-2. Результат: НОД(723, 288) = 3. Эта процедура хороша как для вычисления вручную, так и для программной реализации. Соответствующая программа короткая и быстрая; кроме того, при ее написании трудно ошибиться. Следующие свойства целых чисел и наибольшего общего делителя открывают - по крайней мере теоретически - новые возможности для улучшения программной реализации алгоритма: (а) если числа а и b четные, то НОД(я, Ь) = НОД(я/2, Ь/2) • 2, (Ю.2) (б) если а четное и b нечетное, то НОД(я, Ь) = НОД(а/2, Ь),
190 Криптография на Си и C++ в действии (в) НОД(а, Ь) = НОД(а - Ь, 6), (г) если числа а и b нечетные, то а - b четное и\а-Ь\< max(a, b). Преимущество следующего алгоритма, опирающегося на эти свойства, состоит в том, что в нем используются лишь операции сравнения длин, вычитания и сдвига CLINT-объектов, не требующие особых временных затрат. Для их реализации у нас есть хорошие функции; кроме того, здесь не нужно делить. Бинарный алгоритм Евклида вычисления наибольшего общего делителя можно найти также в книгах [Knut] (п. 4.5.2, Алгоритм В) и [Cohe] (п. 1.3, Алгоритм 1.3.5) почти в такой же форме. Бинарный алгоритм Евклида вычисления НОД(я, Ъ) для а, Ь > О 1. Если а<Ь,то поменять местами а и Ь. Если b = 0, то результат: а и алгоритм заканчивает работу. В противном случае положить к<г-0 и, пока числа а и b оба четные, полагать к<г-к+1, а <— а/2, b <r- Ь/2. (Свойство (а) исчерпано; хотя бы одно из чисел а и b стало нечетным.) 2. Пока а четное, повторять а <— а/2, пока а не станет нечетным. Или, если b четное, повторять b <— b/2, пока b не станет нечетным. (Свойство (б) исчерпано; числа а и b теперь оба нечетные.) 3. Положить t<r-(a- b)/2. Если t = 0, то результат: 2ка и завершить алгоритм. (Здесь мы использовали свойства (б), (в) и (г).) 4. Пока t четное, повторять t <— г/2, пока t не станет нечетным. Если t > 0, то положить a<r-t\ иначе положить b < t. Вернуться на шаг 3. Этот алгоритм легко превращается в программу. Воспользуемся предложением Коха [Cohe] и выполним на шаге 1 дополнительное деление с остатком, положив г <— a mod b, a <— b и b <— г. Тем самым мы уравняли длины операндов а и Ь, поскольку различие в размерах могло бы неблагоприятно сказаться на времени работы программы. Наибольший общий делитель void gcdj (CLINT aa_l, CLINT bbj, CLINT ccj); aaj, bbj (операнды) ccj (наибольший общий делитель) void gcdj (CLINT aaj, CLINT bbj, CLINT ccj) {
I I ГЛАВА 10. Основные теоретико-числовые функции 191 CLINT a J, bj, r_l, tj; unsigned int k = 0; int sign_of_t; Шаг 1. Если аргументы не равны, то меньший аргумент записывается в b_l. Если значение b_l равно 0, то наибольшим обшим делителем будет a_l. if (LT_L (aa_l, bb_l)) { cpyJ (aj, bbj); cpyj (bj, aaj); } else { cpyj (aj, aaj); cpyj (bj, bbj); } if (EQZ_L (bJ)) { cpyj (ccj, aj); return; } f ) | Выполняем деление с остатком, «укорачивая» больший операнд а I. Затем исключаем из а I и b I степени двойки. (void) divj (aj, bj, U, rj); cpyj (aj, bj); cpyj (bj, rj); if (EQZ_L (bj)) { cpyj (ccj, aj);
192 '__ Криптография на Си и C++ в действии return; while (ISEVEN_L (aj) && ISEVEN_L (bj)) { ++k; shrj (aj); shrj (bj); } Шаг 2. while (ISEVEN_L (aj)) { shrj (aj); } while (ISEVEN_L (bj)) ( ) shrj (bj); } Шаг З. Здесь мы сравниваем aj и bj и учитываем, что разность этих чисел может быть отрицательной. Абсолютную величину разности записываем в tj, а знак разности - в целочисленную переменную sign_of_t. Как только t_l == 0, алгоритм завершается. do { if (GE_L (aj, bj)) { subj (aJ, bj, tj); sign_ofJ = 1; else
ГЛАВА 10. Основные теоретико-числовые функции 193 { У к'" ' sub J (b_l, a J, tj); sign_of_J = -1; if (EQZ_L (tj)) { cpyj (ccj, aj); /* ccj <- a */ shiftj (ccj, (long int) k);/* ccj <- ccj*2**k */ return; } Шаг 4. В зависимости от знака переменной tj записываем ее либо в a_l, либо в b_l. while (ISEVEN_L (tj)) { shrj (tj); } if (-1 == sign_of_t) { cpyj (b_l, tj); } else { cpyj (aj, tj); } } while (1); Все используемые здесь операции линейно зависят от числа разрядов операндов, однако тестирование показывает, что обычный алгоритм Евклида из двух строк (см. стр. 189), реализованный в виде функции FLINT/C, значительно медленнее, чем только что рассмотренный вариант. Это странное явление мы можем объяснить
194 Криптография на Си и C++ в действии лишь тем, что, во-первых, наша программа деления не слишком эффективна, а во-вторых, последняя версия алгоритма имеет не- сколько более сложную структуру. Вычисление наибольшего общего делителя для большего числа ар. гументов можно осуществлять путем многократного применения функции gcdj(), так как, согласно свойству (10.1, (в)), общий слу. чай рекурсивно сводится к случаю двух аргументов: (10.3) НОД(/2Ь ..., пг) = НОД(/2Ь НОД(>г2, ..., пг)). Используя наибольший общий делитель, определим наименьшее общее кратное (НОК) двух СLINT-объектов a_l и b_l. Наименьшим общим кратным ненулевых целых чисел щ, ..., пг называется наименьший элемент множества {т е !N+ | щ делит ш, где i = 1, ..., г]. Это множество непусто, поскольку содержит по крайней мере произведение РХГ=1|л/| • Наименьшее общее кратное двух чисел a, be Ж равно частному от деления абсолютной величины произведения на наибольший общий делитель: (10.4) НОК(а, Ъ) • НОД(а, Ъ) = \аЪ\. Воспользуемся этим соотношением для вычисления наименьшего общего кратного чисел aj и bj. Функция: Наименьшее общее кратное (НОК) Синтаксис: int IcmJ (CLINT a J, CLINT bj, CLINT с J); Вход: aj, bj (операнды) Выход: cj (наименьшее общее кратное) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения Int IcmJ (CLINT a J, CLINT bj, CLINT cj) { CLINT g_l, junkj; if (EQZ^L (aJ) || EQZ_L (bj)) { SETZERO_L (cj); return E_CLINT_OK; }
ГЛАВА 10. Основные теоретико-числовые функции 195 gcdj (aJ, bj, gj); divj (aJ, gj, gj, junkj); return (mulj (gj, bj, c_l)); } Случай вычисления наименьшего общего кратного для большего числа аргументов также можно рекурсивно свести к случаю двух аргументов: (10.5) НОКОь ..., пг) = НОК(ль НОК(я2, .... пг)). но формула (10.4) не может быть расширена на большее, чем 2, '^''"- число аргументов, как видно из простейшего примера: 1\ НОК(2, 2, 2) • НОД(2, 2, 2) = 4 Ф 23. Тем не менее, соотношение ме- 1 *' жду наибольшим общим делителем и наименьшим общим кратным можно обобщить на случай большего числа аргументов: (10.6) НОК(а, Ь, с) • ¥ЮД(аЬ, ас, be) = \abc\ и (10.7) НОД(я, Ь, с) • ¥ЮК(аЬ, ас, be) = \abc\ Интересная связь между наибольшим общим делителем и наименьшим общим кратным проявляется в следующих формулах, отражающих двойственность этих функций в том смысле, что, если поменять наибольший общий делитель и наименьшее общее кратное местами, формулы по-прежнему будут верны, как в случаях (10.6) и (10.7). Справедлив дистрибутивный закон: (10.8) НОД(а, НОК(6, с)) = НОК(НОД(я, Ь), НОД(а, с)), (Ю.9) НОК(а, НОД(6, с)) = НОД(НОК(а, Ь), ЯОК(а, с)), и, в довершение всего (см. [Schr], п. 2.4), <Ю- Ю) НОК(я, НОД(6, с)) = НОД(НОК(я, Ь), НОК(я, с)), Эти формулы не только завораживают своей симметрией, но и прекрасно подходят для тестирования функций, касающихся наибольшего общего делителя и наименьшего общего кратного, когда неявно тестируются и арифметические функции (о тестировании см. главу 12). Не вините тестировщиков в том, что они находят ваши ошибки. Стив Мэгью
196 Криптография на Си и C++ в действии 10.2. Обращение в кольце классов вычетов пыкоь 4-ч \НШ-' В отличие от множества целых чисел, в кольце классов вычетов при определенных условиях можно находить мультипликативно обратные элементы. Точнее, в кольце Жп для элемента ае Жп (вообще говоря, не для каждого) существует элемент хе Жп такой, что а • х = 1. Равенство для классов вычетов эквивалентно сравнению а • х = 1 mod п или, иначе, равенству а - х mod п = 1. Например, в кольце Zh элементы 3 и 5 мультипликативно обратны друг другу, поскольку 15 mod 14 = 1. Существование мультипликативно обратных элементов в кольце Жп не очевидно. В главе 5 (стр. 84) мы определили, что (Z„, -^вляется лишь конечной коммутативной полугруппой с единицей 1. Достаточное условие того, чтобы для элемента ае Жп существовал мультипликативно обратный, можно вывести из алгоритма Евклида. Преобразуем предпоследнее равенство алгоритма (см. стр. 189) I «т-2 = Ят-1<]т-2 + ^т, 0 < йт < ат_\, к виду ат = ат-г - ^т-\Чт-ъ (1) Продолжая в том же духе, получаем последовательно ат-\ - ат-з - я,п-2<7ш-з> (2) Ят-2 = Ят-4 - Ят-3<7т-4> (3) а3 = ах -a2q\. (in -2) Подставим в формулу (1) выражение для ат_х из правой части формулы (2): #m = Яm-2 ~ 4m-l{am-Z ~ Чт-Ъат-1) ИЛИ ат = (1 + <7m-3<7w-2)flm-2 ~ ат-2^т-3- Продолжив выкладки, получим в формуле (т-2) представление числа ат в виде линейной комбинации начальных значений а\ и #2» коэффициентами при которых будут неполные частные qt из алгоритма Евклида. Таким образом, получаем представление наибольшего общего делителя g = НОД(я, Ь) = и-а + у-Ьв виде линейной комбинации чисел а и b с целыми коэффициентами и и v, причем и по модулю a/g и v по модулю big определены однозначно. Если же для элемента а 6 Жп выполняется условие НОД(а, п) = 1 = и • а + v • п, то отсюда
ГЛАВА 10. Основные теоретико-числовые функции 197 сразу же следует 1 = и • a mod п или, что то же самое, а • и = 1. В этом случае вычет и mod л определен однозначно и, следовательно, и является обратным к а в кольце Жп. Попутно мы нашли условие существования и вывели процедуру вычисления мультипликативно обратного к элементу кольца Жп. Рассмотрим пример. Из проведенных ранее вычислений наибольшего общего делителя НОД(723, 288) после переупорядочения получаем: 3 = 141-6- 23, 6=147-141- 1, 141=288-147- 1, 147 = 723-288- 2. уф Отсюда получаем линейное представление наибольшего общего делителя: 3=141-23- (147 -141) = 24- 141-23- 147 = = 24- (288-147)-23- 147 = -47- 147 + 24- 288 = г о ■ =-47- (723-2- 288)+ 24- 288 = -47- 723+118- 288. >Г| Быстрая процедура поиска линейного представления наибольшего Ueh общего делителя подразумевает вычисление и запоминание непол- , { ,,, :, ных частных qt (что мы только что и сделали) и восстановление с их п помощью коэффициентов линейного представления. Эта процедура , r v требует много памяти, а значит, непрактична. И вот перед нами ти- ч г „ пичная задача, возникающая при разработке и реализации алгорит- . мов: найти компромисс между требуемым временем вычисления и объемом памяти. Для начала нам нужно так изменить алгоритм Евклида, чтобы вычислять наибольший общий делитель и коэффициенты линейного представления одновременно. Как мы видели, для ае Жп существует обратный элемент хе Жп, если НОД(я, п) = 1. ш Верно и обратное утверждение: если для элемента ае Жп сущест- 1 т вует мультипликативно обратный, то НОД(я, п) = 1 (строгое дока- 1 г зательство этого утверждения см. в работе [Nive], доказательство н теоремы 2.13). Отсюда становится понятно, почему так важен вопрос об отсутствии общих делителей (о взаимной простоте) чисел: подмножество Ж* := {ае Жп\ НОД(я, п)= 1} кольца Жт состоящее из элементов а е Жт взаимно простых с п, с определенной на нем операцией умножения является абелевой группой. В главе 5 мы обозначили эту группу через (Жп\ •)• На группу (Z„\ •) переносятся все свойства абелевой полугруппы (Жп, •) с единицей: • ассоциативность, • коммутативность, • существование единицы: для любого ае Жп выполняется а • 1 = а.
198 Криптография на Си и C++ в действиц — «^ Условие существования мультипликативно обратного также выполняется для всех элементов, поскольку именно такие элементы мы и выбирали. Так что теперь нам осталось проверить лишь замк- нутостъ по умножению, то есть что для любых элементов я, £ кольца Жп произведение а • Ъ тоже будет элементом кольца Жп. Замкнутость доказывается легко: если элементы а и Ъ взаимно просты с п, то произведение ab не может иметь нетривиального общего делителя с числом п, то есть произведение а • b должно принадлежать множеству Жп*. Группа (Z„\ •) называется группой классов вычетов, взаимно простых с л2.Число элементов группы Ж* или, что то же самое, число целых чисел из множества {1, 2, ..., п - 1}, взаимно простых с п, определяется функцией Эйлера ф(и). Для числа /1, представленного в виде произведения п = р[хpf ...p? различ- "<^; ных простых чисел ри ..., /?„ где числа в[ положительны, функция Эйлера вычисляется как Ф(«)=П^н(Р,-1) (см., например, [Nive], пп. 2.1 и 2.4). Отсюда, в частности, следует, что если число р простое, то в группе Ж* ровно р - 1 элементов.3 Если НОД(я, п) = 1, то согласно теореме Эйлера, которая, в свою очередь, является обобщением малой теоремы Ферма, а^п) = 1 mod n, так что, найдя значение ат~х mod n, тоже можно определить мультипликативно обратное к а.4 Например, если п-р • q, где числа р и q простые, р Ф q мае Ж*, то ^^"1)(<у_1) = 1 mod n и, следовательно, вычет я^1^-1)-1 mod n является мультипликативно обратным к а по модулю п. Однако для вычисления этим способом нужно даже в лучшем случае знать значение ф(и), кроме того, сложность модульного возведения в степень равна 0(log3/z). Мы воспользуемся более практичным алгоритмом, который, во- первых, имеет сложность 0(\og2n), а во-вторых, не требует знания функции Эйлера. Для этого объединим приведенную выше процедуру с алгоритмом Евклида. Рассмотрим две переменных и и v, для которых следующий инвариант щ = иг а + у,- • Ъ, А Щ 2 3 Чаще эта группа называется мультипликативной группой кольца. - Прим. перев. В этом случае Жр является полем, поскольку обе группы (Zp, +) и (Z/, -) = (Zp\{0},-) являются абелевыми (см. [Nive], п. 2.11). Конечные поля используются, например, в теории кодирования, а уж их роль в современной криптографии трудно переоценить. Малая теорема Ферма утверждает, что если число р простое, то для любого целого а справедливо ар = a mod р. Если а не делится на р, то ар~1 = 1 mod/? (см. [Bund], глава 2, §3.3). Малая теорема Ферма и ее обобщение — теорема Эйлера — стоят в ряду наиболее важных теорем в теории чисел.
ГЛАВА 10. Основные теоретико-числовые функции 199 будет справедлив на каждом шаге процедуры, описанной на стр. 189, где Я/+1 = а/_1 mod я,-. По завершении алгоритма этот инвариант и даст нам коэффициенты линейного представления наибольшего общего делителя чисел а и Ь. Эта процедура называется расширенным алгоритмом Евклида. Следующий алгоритм заимствован из книги [Cohe], п. 1.3, Алгоритм 1.3.6. Переменная v присутствует в нем неявно и вычисляется лишь в конце алгоритма как v :=(d-u • a)lb. «*. Расширенный алгоритм Евклида вычисления НОД(а, Ъ) и чисел и и v таких, что НОД(а, b) = u a + v b, для а, Ь > 0 1. Положить и <— 1, d <— а. Если Ъ - 0, то положить v <— 0 и завершить алгоритм; иначе положить vi <— 0 и v3 <— Ъ. 2. Вычислить q и 13 такие, что d - q • v3 + h и t3 < v3, поделив d с остатком на v3. Положить t\ <— и - q • vb и <— vb d <— v3, Vi <— ^ и v3<-^3. 3. Если v3 = 0, то положить v <r-(d-u- a)lb и завершить алгоритм; иначе вернуться на шаг 2. ":* Построим функцию xgcdJO с использованием вспомогательных функций sadd() и ssub(), вычисляющих знаковое сложение и вычитание (в исключительных случаях). Каждая из этих функций предварительно определяет знак своего аргумента, а затем вызывает ба- га зовые функции add() и sub() (см. главу 5), выполняющие, соответственно, сложение и вычитание без учета переполнения или потери значащих разрядов. Кроме того, на основе функции деления div_J(), определенной для натуральных чисел, создадим вспомогательную функцию smod() для вычисления вычета a mod 6, где a, be Ж и b > 0. Эти вспомогательные функции еще пригодятся нам при построении функции chinremJO, реализующей китайскую теорему об остатках (см. п. 10.4.3). При возможном обобщении библиотеки FLINT/C на целые числа этими функциями можно воспользоваться для работы со знаковыми типами. Порядок использования функции xgcdJO следующий: если оба аргумента удовлетворяют условию a, b > Nmax/2, то в значениях и и v, возвращаемых функцией xgcdJO, может возникнуть переполнение. Для разрешения подобных ситуаций следует запасти место для этих значений, которые в этом случае будут объявлены вызывающей программой как переменные типа CLINTD или CLINTQ (см. главу 2).
200 Криптография на Си и C++ в действии Функция: Синтаксис: Вход: Выход: Расширенный алгоритм Евклида вычисления линейного представления НОД(а, Ь) = и - а + v • b для натуральных чисел a, b void xgcdj (CLINT a_, CLINT bj, CLINT g_l, CLINT uj, int *sign_u, CLINT vj, int *sign_v); a J, bj (операнды) gj (наибольший общий делитель чисел а_1 и bj) uj, vj (коэффициенты при а_1 и Ь_1 в линейном представлении числа gj) *sign_u (знак коэффициента uj) *sign_v (знак коэффициента vj) void r' " l xgcdj (CLINT aj, CLINT bj, CLINT dj, CLINT uj, int *sign_u, CLINT vj, int *sign_v) CLINT v1_l, v3_l, t1_l, t3_l, qj; CLINTD tmpj, tmpuj, tmpvj; int sign_v1, sign_t1; Шаг 1. Задание начальных значений. cpyj (dj, aj); cpyj (v3_l, bj); if (EQZ_L (v3J)) { SETONE_L (uj); SETZERO^L (vj); *sign_u = 1; *sign_v= 1; return; } SETONE_L (tmpuj); *sign_u = 1;
ГЛАВА 10. Основные теоретико-числовые функции 201 •лэт •aiv П •ТООН Ту;; SETZERO_L(v1_l); sign_v1 = 1; Шаг 2. Основной цикл; вычисление наибольшего обшего делителя и коэффициента и. while (GTZ_L (v3J)) { divj (dj, v3J, qj, t3J); mulj (v1_l, qj, qj); sign_t1 = ssub (tmpuj, *sign_u, qj, sign_v1, t1_l); cpyj (tmpuj, v1_l); *sign_u = sign_v1 ; cpyj (dj, v3J); cpyJ(v1J,t1J); sign_v1 = sign_t1; cpyj (v3_l, t3_l); } Шаг З. Вычисление коэффициента v и завершение процедуры. mult (a_l, tmpuj, tmpj); *sign_v = ssub (dj, 1, tmpj, *sign_u, tmpj); divj (tmpj, bj, tmpvj, tmpj); cpyj (uJ, tmpuj); cpyj (vj, tmpvj); return; }
202 _ Криптография на Си и C++ в действии Поскольку обработка отрицательных чисел в пакете FLINT/C требует дополнительных затрат, здесь нам полезно заметить, что для вычисления мультипликативно обратного к классу вычетов а е Жп* из линейного представления 1 = и • а + v • Ъ нам нужен только коэффициент и. Найдя для и соответствующий положительный представитель, мы избавимся от необходимости оперировать с отрицательными числами. Следующий алгоритм представляет собой вариант предыдущего, учитывает наше замечание и полностью исключает вычисление коэффициента v. Расширенный алгоритм Евклида вычисления НОД(а, Ъ) и мультипликативно обратного к a mod п для а > О, п > О 1. Положить и <— 1, g <— a, v{ <— 0 и и Уз <— п. 2. Вычислить q и гъ такие, что g = q • v3 + ^ и h < ^з> поделив d с остатком на v3. Положить t\ <— и - q • v\ mod л, и <— vb g <— v3, Vj <— /j И V3 <— t3. 3. Если v3 = 0, то результат: g = НОД(а, Z?), элемент и мультипликативно обратный к a mod n и алгоритм завершен. Иначе вернуться на шаг 2. Шаг tx<r-u-q-vi mod п гарантирует, что числа гь vi и и будут неотрицательными. По окончании алгоритма получаем: ие{1,...,я-1}. Реализуем этот алгоритм в виде следующей функции. Функция: вычисление мультипликативно обратного в кольце Z„ I Синтаксис: void invj inv_l(CLINT a_l, CLINT nj, CLINT gJ.CUNT U); | Вход: aj, nj (операнды) | Выход: gj (наибольший общий делитель чисел а_1 и nj) iJ (обратный элемент к aj mod nj, если он существует) void invj (CLINT aj, CLINT nj, CLINT gj, CLINT ij) { CLINT v1_l, v3_l, t1_l, t3_l, qj; ll Проверка операндов на равенство нулю. Если хотя бы один из операндов равен 0, то обратного элемента не существует, чего нельзя сказать о наибольшем обшем делителе (см. стр. 188). В этом случае результируюшая переменная М не определена и полагается равной нулю.
ГДАВА 10. Основные теоретико-числовые функции 203 if (EQZ_L (aJ)) эп if (EQZ_L (n_l)) mm-' i SETZERO_L (gj); SETZERO_L (ij); return; } else { cpyj (gj, nj); г SETZERO_L (ij); i« return; .<>< } } else { if (EQZ_L (nJ)) cpyj (gj, aj); *Г SETZERO_L (ij); м return; } } Шаг 1. Задание начальных значений. cpyj (gj, aj); cpyj (v3J, nj); SETZERO_L(v1J); SETONEJ_(t1J); do {
i J 204 Криптография на Си и C++ в действии Шаг 2. После деления осуществляем проверку в GTZ_L(t3_l), что позволяет избежать лишнего вызова функций mmulJO и msub_l() при последнем прохождении цикла. До окончания цикла переменной М ничего не присваиваем. divj (g_l, v3_l, qj, t3J); if (GTZ_L (t3J)) { mmulj (v1 J, qj, qj, nj); msubj (t1_l, qj, qj, nj); cpyj (t1J, v1_l); cpyj (v1_l, qj); cpyj (g_l, v3J); cpy_l (v3_l, t3J); } } while (GTZ_L (t3J)); Шаг З. Выполняем последнее присваивание. В качестве наибольшего обшего делителя берем значение переменной v3_l, и если оно равно 1, то в качестве обратного к a_l берем значение переменной v1 I. cpyj (g_l, v3_l); if (EQONEJ. (gj)) { cpyj (U, v1 J); } else { SETZERCLL (ij); } }
ГЛАВА 10. Основные теоретико-числовые функции 205 10.3. Корни и логарифмы ■•ни ■ '-ни :;■■. В этом параграфе мы научимся вычислять целую часть квадратного корня и логарифмы по основанию 2 для объектов типа CLINT. Сначала рассмотрим вторую из этих функций, а затем с ее помощью будем вычислять первую: для натурального числа а будем искать число е такое, что 2е < а < 2е+[. Число е = |_log2 a\ есть целая часть логарифма числа а по основанию 2 и равно уменьшенному на 1 числу значащих битов числа а. Для этого используется функция ld_l(), входящая в состав многих других функций пакета FLINT/C, которая игнорирует ведущие нули и считает только значащие двоичные разряды CLINT-объекта. функция: Синтаксис: Вход: Выход: Число значащих двоичных разрядов CLINT-объекта unsigned int ld_l (CLINT aj); aj (операнд) Число значащих двоичных разрядов числа а_1 unsigned int ld_l (CLINT nj) { unsigned int I; USHORT test; 'iff. Шаг 1. Определяем число значащих разрядов в системе счисления по основанию В. I = (unsigned int) DIGITS_L (nj); while (n_l[l] == 0 && I > 0) { } if (I == 0) { return 0; }
206 Криптография на Си и C++ в действии ft*y*s г-***&ш«е,а Шаг 2. Определяем число значащих битов в старшем разряде. Константа BASEDIV2 - это число, имеющее единичный старший бит и остальные нули (то есть 2 BITPERDGT-1 ). test = n_l[l]; I «= LDBITPERDGT; while ((test & BASEDIV2) == 0) { test «= 1; } return I; } i Теперь перейдем к вычислению целой части квадратного корня из натурального числа. Воспользуемся классическим методом Ньютона (называемым еще методом Ньютона-Рафсона), который обычно применяется для определения нулей функции путем последовательных приближений. Пусть функция fix) дважды непрерывно дифференцируема на интервале [а, Ь] так, что первая производная /'(*) на этом интервале положительна и max [a,b] /(*)•/'(*) fix)2 < 1. Тогда, если хп е [а, Ь] - приближение к числу г, где fir) = 0, то значение хп+\ := хп -fixn)lf'(x,) будет к г ближе, чем хп. Последовательность приближений хп сходится к г (см. [Endl], п. 7.3). Если положить fix) := х2 - с, где о 0, то для х > 0 функция fix) будет удовлетворять условиям сходимости метода Ньютона, а последовательность •^-и-1-1 •"*■ Хп хп +- п J будет сходиться к Таким образом, получаем эффективную процедуру для приближения квадратных корней рациональными числами. Нас интересует только целая часть г числа л/с , где г2 < с < (г + 1) , а число с натуральное, поэтому при вычислении элемента последовательности приближений ограничимся только целой его частью.
рддВА 10. Основные теоретико-числовые функции 207 В качестве начального приближения выберем хх>^с и будем продолжать до тех пор, пока для некоторого п не получим xn+i > хп, тогда хп и будет искомым значением. Естественно выбрать начальное приближение как можно ближе к . Для значения с типа CLINT и е := Llog2cJ получаем, что значение [_2c"+2)/2J всегда больше, чем , то есть начальное приближение можно легко вычислить с помощью функции ld_l(). А вот и алгоритм. ".' O'-j ■■ИНГ * нг. Алгоритм вычисления целой части г квадратного корня из натурального числа о 0 1. Положить х <- [_2("+2)/2 J, где е := Llog2cJ. 2. Положить у <— [(x + c/x)/2J. 3. Если у < х, то положить х <— у и вернуться на шаг 2. Иначе результат: х и алгоритм завершен. Доказательство корректности алгоритма не представляет труда. Значение х изменяется монотонно и всегда является натуральным числом. Следовательно, алгоритм в конце концов остановится. По завершении алгоритма будет выполняться условие у = \(х + c/x)/2J> x . Предположим, что х > г + 1. Из условия х > г + 1 > V с следует, что х2 > с или, иначе, с - х2 < 0. Однако, ■ х- (х + с/х) ■ х = 2х <0, что противоречит условию завершения алгоритма. Значит, наше предположение неверно их= г. Следующая функция корректно вычисляет значение у <—1_(* + с/х)/2J, используя целочисленное деление с остатком. Функция: Синтаксис: Вход: Выход: Целая часть квадратного корня из СLINT-объекта void irootj (CLINT nj, CLINT floorj); nj (операнд > О) floorj (целое число - квадратный корень из nj) void irootj (CLINT nj, CLINT floorj) {
208 Криптография на Си и C++ в действии ^ .' CLINTxJ, у_1, г_1; unsigned I; Используя функцию ld_l() и оператор сдвига, полагаем I равным L( Uog2(n_l)J + 2)/2j. Используя функцию setbitJO, полагаем у J равным 21. I = (ldj(nj) + 1)»1; SETZERO_L (yj); setbitj (yj, I); do { cpyj (x_l, yj); Шаги 2 и З. Аппроксимация методом Ньютона и проверка условия завершения алгоритма. divj (n_l, xj, yj, M); addj (yj, xj, yj); shr_l (yj); } while (LT_L (yj, xj)); cpyj (floorj, xj); } Чтобы выяснить, является ли число п квадратом какого-либо числа, достаточно возвести в квадрат с помощью функции sqr_l() значение функции irootJO и сравнить полученный результат с п. Если числа не совпадают, то, очевидно, п не является квадратом. Признаем, все же, что это не самый лучший метод. Существуют критерии, позволяющие во многих случаях распознать, является ли число квадратом, без вычисления квадратного корня. Один из таких алгоритмов приведен в работе [Cohe]. Строятся четыре таблицы: qll, q63, <y64 и q65, в каждой из которых квадратичные вычеты по модулю И» 63, 64 и 65 помечены единицей «1», а квадратичные невычеты - нулем «О». <71Щ]<-Одля£ = 0, ..., 10, д63[к] <- 0 для к = 0, ...,62, <7ll[£2mod 11] <- 1 для к = 0, ...,5, <763[£2mod63]<- 1 для/: = 0, ...,31,
ГЛАВА 10. Основные теоретико-числовые функции 209 л и q64[k] <- 0 для к = 0, ...,63, 4б5[£]<-0для& = 0, ...,64, q64[k2 mod 64] <- 1 для £ = 0, ...,31, ^65[к2 mod 65] <- 1 для к = 0, ..., 32. Нетрудно заметить, что, рассматривая кольцо классов вычетов как систему абсолютно наименьших вычетов (см. стр. 84), получаем таким образом все квадраты. Алгоритм, определяющий, является ли целое число п > 0 полным квадратом. Если да, то результат алгоритма - квадратный корень из числа п (|Cohe], Алгоритм 1.7.3) 1. Положить t <r- n mod 64. Если q64[f] = 0, то число п не является полным квадратом и алгоритм заканчивает работу. Иначе положить г <г— п mod (11 • 63 • 65). 2. Если q63[r mod 63] = 0, то число п не является полным квадратом и алгоритм заканчивает работу. 3. Если q65[r mod 65] = 0, то число п не является полным квадратом и алгоритм заканчивает работу. 4. Если qll[rmod 11] = 0, то число п не является полным квадратом и алгоритм заканчивает работу. 5. Вычислить q <— \уп] с помощью функции iroot_l(). Если q2 Ф п, то число п не является полным квадратом и алгоритм заканчивает работу. Иначе п является полным квадратом и результат: q - квадратный корень из /г. На первый взгляд этот алгоритм может показаться странным: что за константы 11, 63, 64, 65? Внесем ясность: если целое число п является полным квадратом целого числа, то оно будет полным квадратом и по модулю произвольного целого числа к. Воспользуемся обратным утверждением: если п не является квадратом по модулю к, то оно не является квадратом и в целых числах. Таким образом, на шагах 1-4 мы проверяем, является ли п квадратом по модулям 64, 63, 65 и И. Всего существует 12 квадратов по модулю 64, 16 квадратов по модулю 63, 21 квадрат по модулю 65 и 6 квадратов по модулю 11, то есть вероятность пропустить за четыре шага число, не являющееся полным квадратом, равна 64 [ 63 [ 65 -1 Yl_ \в_ 21 6_ ' 64 ' 63 ' 65 ' 11 6 715 Только в таких относительно редких случаях выполняется проверка на шаге 5. Если проверка проходит успешно, то п является полным квадратом и квадратный корень из п определен. Очередность модулей на шагах 1-4 определяется соответствующими вероятностями. Появление следующей функции мы предвидели в п. 6.5, когда исключали числа, являющиеся полными квадратами, из множества потенциальных первообразных корней по модулю р.
210 Криптография на Си и C++ в действии функция: Синтаксис: Вход: Выход: Возврат: Является ли число n_l типа CLINT полным квадратом unsigned int issqrj (CLINT nj, CLINT rj); nj (операнд) rj (квадратный корень из n_l или 0, если nj не является полным квадратом) 1, если nj - полный квадрат, О, в противном случае static const UCHAR q 11 [11 ]= {1,1,0,1,1,1,0,0,0,1,0}; 1Д1Ша ^"кИП^'Н, . ПК\йВ? ШГ.'п static const UCHAR q63[63]= {1, 1,0,0, 1,0,0, 1,0, 1,0,0,0,0,0,0, 1,0, 1,0,0,0,1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, О, О, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}; static const UCHAR q64[64]= {1, 1,0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1,0, О, О, О, О, О, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, О, О, О, О, О, 0, 0, 1,0, 0, 0, 0, 0, 0, 0, 1, О, О, О, О, О, О}; г>* static const UCHAR q65[65]= {1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, О, О, О, О, О, О, О, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, О, О, О, О, О, 0,0,0, 1,0, 1,0,0,0, 1, 1,0,0,0,0, 1,0,0, 1}; unsigned int issqrj (CLINT nj, CLINT rj) { CLINT qj; USHORT r; if (EQZ_L (nj)) { SETZERCLL (rj); return 1; } n
Г! гдДВА 10. Основные теоретико-числовые функции 211 if (1 == q64[*LSDPTR_L (n J) & 63]) /* q64[n_l mod 64] */ r = umodj (nj, 45045); /* nj mod (11- 63- 65) 7 к ■ - * ' if ((1 == q63[r % 63]) && (1 == q65[r % 65]) && (1 == q11[r % 11])) /* Условия проверяются слева направо, см. [Harb], п. 7.7 */ -Я- '■ЮТ': { iroot_l (nj, rJ); sqrj (rj, qj); if (equ_l (nj, qj)) { return 1; } } } SETZERO_L (rj); I; return 0; } 10.4. Квадратные корни в кольце классов вычетов Итак, мы научились вычислять квадратные корни (по крайней мере, их целые части) из целых чисел. Теперь вновь обратимся к кольцам классов вычетов, в которых займемся тем же самым, а именно, вычислением квадратных корней. При некоторых ограничениях в кольце классов вычетов существуют квадратные корни, вообще говоря, определенные неоднозначно (то есть может существовать несколько квадратных корней из одного элемента). Говоря на языке алгебры, задача состоит в том, чтобы выяснить, существуют ли для элемента ае Жт корни Ъ е Zw такие, что Р = а. Или, в теоретико-числовых обозначениях (см. главу 5), есть ли решения у сравнения второй степени х 2= a mod w, и если да, то какие. Если НОД(а, т) = 1 и существует решение b такое, что b2 = a mod /и, то число а называется квадратичным вычетом по модулю т. Если
"1 212 Криптография на Си и C++ в действии сравнение неразрешимо, то а называется квадратичным невыце^ том по модулю т. Если Ь - решение сравнения, то b + m тоже решение, то есть можно ограничиться рассмотрением вычетов отличающихся на т. Поясним ситуацию на примере. Число 2 является квадратичным вычетом по модулю 7, поскольку З2 = 9 = 2 (mod 7); число 3 является квадратичным невычетом по модулю 5. Если число т простое, то найти квадратные корни по модулю т до- вольно просто, позже мы приведем все необходимые для этого функции. Трудность вычисления квадратных корней по модулю составного числа п определяется тем, известно ли разложение числа т на простые множители. Если разложение неизвестно, то вычисление квадратных корней для большого числа т является математически сложной задачей из класса NP (см. стр. 119), лежащей в основе безопасности ряда современных криптосистем.5 Мы еще вернемся к этому вопросу в п. 10.4.4. Определение того, является ли число квадратичным вычетом, и вычисление квадратных корней - это две разные задачи, для решения каждой из которых существуют свои методы. В следующих параграфах мы приведем реализацию этих методов с подробными комментариями. Сначала рассмотрим процедуры, позволяющие определить, является ли число квадратичным вычетом по модулю данного числа. Затем научимся вычислять квадратные корни по модулю простых чисел и, наконец, по составным модулям. 10.4.1. Символ Якоби Сразу начнем с определения. Пусть число р Ф 2 простое и число а целое. Символ Лежандра \j) (читается «а по /?») равен 1, если а - квадратичный вычет по модулю р, и -1, если а - квадратичный невычет по модулю р. Если а делится на р, то (у):= 0. Это определение пока вряд ли нам поможет, поскольку для того, чтобы знать значение символа Лежандра, нужно знать, является ли а квадратичным вычетом по модулю р. Однако символ Лежандра обладает замечательными свойствами, которые и позволят нам оперировать с ним и, что особенно важно, вычислять его значение. Чтобы не 5 Аналогию между математической и криптографической сложностью следует проводить очень осторожно: согласно работе [Rein], вопрос о справедливости неравенства Р Ф NP весьма мало касается практической криптографии. Полиномиальный алгоритм разложения на множители с временной сложностью 0(п20) бессилен даже перед относительно небольшими значениями числа п, („•■• \ тогда как экспоненциальный алгоритм сложности О \е J справится даже с большими значениями модуля. Безопасность криптографических алгоритмов на практике не зависит от того, совпадают или нет множества Р и NP, несмотря на то, что часто встречается именно такая формулировка.
ГЛАВА Ш. Основные теоретико-числовые функции 213 »*1Э ,.!=*: сбиться с пути, не будем вдаваться в теоретические дебри. Заинтересованный читатель может обратиться, например, к работе [Bund], п. 3.2. Но все же нам придется привести здесь некоторые свойства, дающие основное представление о правилах вычислений с символом Лежандра. (а) Число решений сравнения х2 = a (mod р) равно 1 + (у). (б) Число квадратичных вычетов и невычетов по модулю р одинаково и равно (р- 1)/2. (в) Если а = Ъ (mod р), то \jj= \j). (г) Символ Лежандра обладает свойством мультипликативности: (д) Ш=°- (е) a(p~l)^2 =\^)(modp) (критерий Эйлера). (ж) Если число q нечетное простое и q*p, то (~)= (-l)(/,_1)(<7~1)/4(j) (квадратичный закон взаимности Гаусса). (3)(f)=(-l)<"-|)/2, й=(-1)('1-1^, fr)=l. Доказательство этих свойств можно найти в стандартной литературе по теории чисел, например, в [Bund] или [Rose]. Сразу же приходят в голову два способа вычисления символа Лежандра. Во-первых, воспользоваться критерием Эйлера (свойство (е)) и вычислить а^'^11 (mod p). Для этого потребуется выполнять модульное возведение в степень (со сложностью 0(log /?)). Во-вторых (и это более разумное решение), с помощью квадратичного закона взаимности реализовать следующую рекурсивную процедуру, основанную на свойствах (в), (г), (ж) и (з). Рекурсивный алгоритм вычисления символа Лежандра \ф) для целого а и нечетного простого/; 1. Если а = 1, то (jj = 1 (свойство (з)). 2. Если а четное, то (*■) = (-l)**2-^8^) (свойства (г), (з)). 3. Если аФ\ и а = cji...qk - произведение нечетных простых <7ь...,#ьТ0
214 Криптография на Си и C++ в действии ';ifki^/'. .\"H<WX'I"*J; febnW- 1=1 Для каждого / вычисляем с помощью шагов 1-3 (свойства (в), (г) и (ж)). Прежде чем заняться программной реализацией этого алгоритма, рассмотрим обобщение символа Лежандра. Это позволит нам обойтись без разложения на простые множители, которое необходимо при использовании квадратичного закона взаимности в виде свой- :>она;: г*. ства (ж) и для больших чисел занимающее чересчур много времени (о задаче разложения см. стр. 225). А для этого обратимся к нерекурсивной процедуре и введем еще одно определение. Для целых чисел а и b =рх...рь где числа рх простые, но не обязательно различные, символ Якоби (называемый также символом Якоби - Кро- некера, или символом Кронекера - Якоби, или символом Кронекера) (j) определяется как произведение символов Лежандра Й-J: (t)-nfc). где если а четное, (-1)(а ~1)'8, если а нечетное. Для полноты картины определим (f):=l для аеЖ\ (§-):=1, если а = ±1, и (■§):= 0 в противном случае. Если число b нечетное простое (то есть k= 1), то значения символов Лежандра и Якоби совпадают. В этом случае символ Якоби (Лежандра) показывает, является ли а квадратичным вычетом по модулю Ь, то есть существует ли число с такое, что с2 = a mod Ь. Если такое с существует, то (f)= 1, иначе (f)=-1 (или (f)= 0, если а = 0 mod b). Для составного числа b (при к > 1) число а является квадратичным вычетом по модулю b тогда и только тогда, когда НОД(я, Ь) = 1 и а является квадратичным вычетом по модулю всех простых чисел, делящих /?, то есть когда (-^)=1 для всех / = 1, ..., к- Ясно, что это не эквивалентно равенству (f) = 1. Например, сравнение х2 = 2 mod 3 неразрешимо, то есть (|)= -1. Но по определению (§•)= (уХз')= ! > хотя сравнение х2 = 2 mod 9 также неразрешимо. Обратно, если (g-)= -1, то а в любом случае является квадратичным
гддВА 10. Основные теоретико-числовые функции 215 невычетом по модулю Ъ. Равенство (f) = 0 равносильно тому, что НОД(а,&)*1. I <м • &v.. Пользуясь свойствами символа Лежандра, выведем свойства символа Якоби: ', (a) (f) = (f Хт) и, если Ь ■ с ф 0, то (fr) = (f Xf) • <• ? ч (б) Если а = с mod /?, то (~) = (f). (в) Для нечетных Ъ > 0 справедливы равенства (f)=(-iri)'\ (i)=(-D(*2-1,/8, (|)=i (см. свойство (з) символа Лежандра). (г) Для нечетных а и Ь, где Ъ > 0, выполняется квадратичный закон взаимности (см. свойство (ж) символа Лежандра): (f) = (-\уа-1)(-ь-[У4 \±J Из этих свойств (доказательство см. в литературе, указанной выше) получаем следующий алгоритм Кронекера (см. [Cohe], п. 1.4) вычисления символа Якоби (или, в зависимости от условий, символа Лежандра) для двух целых чисел. Этот алгоритм нерекурсивный. Кроме того, чтобы не зависеть от знаков этих чисел, положим (^):= 1 при а > 0 и (^):= -1 при а < 0. л Алгоритм вычисления символа Якоби (^) для целых чисел а и Ь 'j '' 1. При Ъ = 0 завершить алгоритм с результатом 1, если \а\ = 1, и с \. результатом 0 в противном случае. ёГ: 2. Если оба числа а и Ь четные, то завершить алгоритм с результатом 0. Иначе положить v <— 0 и, пока Ъ четное, выполнять: v <— v + 1 и b <— b/2. Если теперь v четное, то положить к <— 1; иначе положить к <— (-1)(а"_1)/8. При Ъ < 0 положить Ъ <—Ъ. При а < 0 положить к < к (см. свойство (в) символа Якоби). 3. При а = 0 завершить алгоритм с результатом 0, если Ь > 1, с результатом /: в противном случае. При а Ф 0 положить v <— 0 и, пока а четное, выполнять: v <— v + 1 и а <— all. Если теперь v нечетное, то положить к <— (-1)(/г_1)/8 •& (см. свойство (в) символа Якоби). 4. Положить к <г-(-\){а-1)ф-Х)1А - к , r<-|a|, a <- 6 mod r, b <-г и вернуться на шаг 3 (см. свойства (б) и (г) символа Якоби). Время работы этого алгоритма равно 0(\og2N), где число N>a,b- верхняя граница чисел а и Ь. Это значительно лучше, чем вычислено
216 Криптография на Си и C++ в лействиц ние по критерию Эйлера. А вот еще два штриха, улучшающие этот алгоритм (см. [Cohe] п. 1.4): • Для вычисления значении (-1) и \гч на шагах 2 и 3 лучше заготовить таблицу предвычислений. • Значение (-1)(а~1)(/,_1)/4 .£ на шаге 4 можно вычислить на языке С как if (a&b&2) k = -к, где & - это поразрядная операция AND. В обоих случаях не нужно явно возводить в степень, что, конечно, благоприятно сказывается на времени работы алгоритма. Остановимся на первом «штрихе» более подробно. Если на 2 мы полагаем к равным (-1){а _1)'8, то а нечетное. То же справедливо в отношении Ъ на шаге 3. Для нечетного а 2|(я-1) и 4|(а+1) или 4|(я-1) и 2|(я+ 1), то есть 8 делит произведение {а - \){а + 1) = а2 - 1 и число (а2 - 1)/8 - целое. Кроме того, выполняется равенство (-l)(fl"_1)/8 = (_^((«mod8) -w8 (для проверки достаточно подставить в показатель степени выра- * жение а = к • 8 + г). Следовательно, показатель определяется лишь четырьмя значениями числа a mod 8 = ±1 и ±3, дающими соответственно результаты 1,-1,-1 и 1. Записываем их в виде вектора {О, 1, 0, -1, 0, -1, 0, 1}, откуда, зная a mod 8, можем найти значение £_iy(amod8)--i)/8 Заметим, что a mod 8 можно представить в виде выражения а & 7, где & по-прежнему означает бинарную операцию AND, то есть возведение в степень сводится к нескольким быстрым процессорным операциям. Для пояснения второго «штриха» заметим, что (а & b & 2) ф 0 тогда и только тогда, когда числа (а - 1)/2 и {Ь - 1)/2, а значит и (а - \){Ь - 1)/4, нечетные. И, наконец, введем вспомогательную функцию twofactJO для вычисления значений v и Ь на шаге 2 для четного Ь, а также v и а на шаге 3 для четного а. Функция twofactJO находит представление числа типа CLINT в виде произведения степени двойки и нечетного числа.
ГЛАВА 10. Основные теоретико-числовые функции 217 функция: Представление CLINT-объекта в виде а = 2 и, где число и нечетное Синтаксис: int twofactj (CLINT aj, CLINT b_l); Вход: aj (операнд) Выход: b_l (нечетная часть числа aj) Возврат: к (логарифм по основанию 2 в разложении числа aj) int twofactj (CLINT a J, CLINT bj) { int k = 0; if (EQZ_L (aJ)) { SETZERCLL (bj); return 0; i **' **,« ■«* фУнкция: Синтаксис: вход: возврат: cpy_l (bj, aj); while (ISEVEN_L (bj)) { shrj (bj); ++k; } return k; } Теперь мы вооружены всем необходимым для реализации функции jacobi_l(), вычисляющей символ Якоби. Вычисление символа Якоби от двух СLINT-объектов int jacobij (CLINT aa_l, CLINT bbj); aaj, bbj (операнды) ±1 (значение символа Якоби aaj no bbj) Нетрудно заметить, что она возвращает еще и 0. - Прим. ред.
218 Криптография на Си и C++ в действии г t»,.-.r-.«1 static int tab2[] = {0, 1, 0, -1, 0, -1, 0,1}; int jacobij (CLINT aaj, CLINT bbj) { CLINT aj, bj, tmpj; long int k, v; Шаг 1. Случай bbj == 0. if (EQZ_L (bbj)) { if (equj (aaj, onej)) { return 1; } else { return 0; } } Шаг 2. Удаляем четную часть переменной bbj. if (ISEVEN_L (aaj) && ISEVEN_L (bbj)) return 0; i cpyj (aj, aaj); cpyj (bj, bbj); v = twofactj (bj, bj); if ((v & 1) == 0) Л v четное? */ {
гдАВА 10. Основные теоретико-числовые функции 219 к = 1; } else { к = tab2[*LSDPTR_L (aj) & 7]; /* *LSDPTR_L (aj) & 7 == aj % 8 7 } Шаг 3. Если a_l == 0, то завершаем алгоритм. Иначе удаляем четную часть переменной aj. while (GTZ_L (aJ)) { v = twofactj (aj, aj); if ((v&1)!=0) { к = tab2[*LSDPTR_L (bj) & 7]; } Шаг 4. Применяем квадратичный закон взаимности. if (*LSDPTR_L (aj) & *LSDPTR_L (bj) & 2) { k = -k; } cpyj (tmpj, aj); modj (bj, tmpj, aj); cpyj (bj, tmpj); H:m I - ' } £Я>| v if (GT_L (bj, onej)) { k = 0; } return (int) k; Г
220 Криптография на Си и C++ в действии 10.4.2. Квадратные корни по модулю рк Теперь мы знаем, что целое число может быть квадратичным вычетом или невычетом по модулю другого целого числа. Более того, у нас есть эффективная программа, определяющая, какой из этих случаев имеет место. Но даже зная, что целое число а является ^ квадратичным вычетом по модулю целого /г, мы пока не умеем из- влекать квадратный корень из я, особенно если число п большое. Будем скромны и попытаемся сначала сделать это для простых п. Таким образом, наша задача - решить сравнение второй степени (10.11) x2 = amodp, где число р нечетное простое и а - квадратичный вычет по модулю р (это гарантирует нам, что сравнение разрешимо). Рассмотрим два случая: р = 3 mod 4 ир=1 mod 4. В первом, более простом случае решением сравнения будет х := я^+1)/4 mod p, поскольку (10.12) / s а^т ^ а • а^12 ^ a mod />, где a{p~X)l2 =(j)=lmod/? мы вычислили по свойству (е) символа Лежандра (критерий Эйлера). Следующие рассуждения, заимствованные из [Heid], приводят нас *"«•*• к общей процедуре решения сравнений второй степени, в том числе и в случае р = 1 mod 4. Представим р - 1 в виде р - 1 = 2kq, где м к > 1 и число q нечетное. Найдем произвольный квадратичный невычет п mod р, выбирая случайное и, 1 < п < р, и вычисляя символ 4*v Лежандра (jj. Значение -1 мы получим с вероятностью у, то есть нужное п будет найдено довольно быстро. Положим x0^^/+1)/2mod/7, (Ю-1*) y0 = n(,modp, Zo = a1 mod p, r0 := L Согласно малой теореме Ферма а^^2 = х2{р~т = хГх = 1 mod p9 если х - решение сравнения (10.11). Кроме того, если п - квадратичный невычет по модулю р, то /^"1)/2 = -1 mod p (см. свойство (е) символа Лежандра, стр. 213). Тогда
гддВА 10. Основные теоретико-числовые функции 221 (10.14) az0 =xl mod p, 'гл> yl = -1 mod /7, z0 =l mod p. '% - Если zo = 1 mod/?, то х0 будет решением сравнения (10.11). Иначе определим рекуррентные последовательности xi9 yi9 z,-, /■/, где (70.75) aZj =xf mod p, yf =-lmod/?, z,- = lmod/7 и г, > rM. Сделав не более чем к шагов, получим z, = 1 mod/?, тогда xi будет решением сравнения (10.11). Для этого найдем т0 - 2'"° 1 А наименьшее натуральное число, для которого z0 = lmodp, то есть т0<г0- 1. Положим (Ю.16) хм=х^~"ч~ mod р, Ум = У! mod Р> */+i s ад2" "" mod p, где г,, := т, := mm р > 11 zf = 1 mod p j. Тогда #/y */+i = *,- Уi = as,- J/ = azM mod /7, «, 9"'i-l 2-i+i-i 2»f-i / 2г;-,„; \ 2r,-\ Ум =Ум =\У1 =У1 s-lmodp. z/+i sz,+I = z,j, s-Z/ = lmodp, ^ j = zf =1 mod p , и значит, в силу минимально- сти mh возможен лишь случай z, = -1 mod p . Таким образом, доказана корректность алгоритма Д. Шенкса (D. Shanks), определяющего решение сравнения второй степени (см. [Cohe], Алгоритм 1.5.1).
222 Криптография на Си и C++ в действии Алгоритм извлечения квадратного корня из целого числа а по модулю нечетного простого/? 1. Записать р - 1 в виде 2kq, где число q нечетное. Найти случайное п такое, что (j)=-l. 2. Положить х <— а(</~1)/2 mod р, у <- л'7 mod p, z<r-a • х2 mod p, : |: .., х <— а - х mod р и г <г- к. 'у- 3. Если z = 1 mod p, то завершить алгоритм с результатом х. Иначе 2'" найти наименьшее w, для которого z =1 mod /? . При т = г завершить алгоритм с результатом «а - квадратичный невычет по модулю р». 4. Положить t <— у mod/?, у <— г mod/7, r<—mmodp, х <— х • t mod/?, z <— г • у mod p и вернуться на шаг 3. Ясно, что если х - решение сравнения второй степени, то -х mod p тоже будет решением этого же сравнения, так как (-х) = х mod p. В следующей программной реализации, не задумываясь о практичности, для всех подряд натуральных чисел, начиная с 2, будем вычислять символ Лежандра, надеясь найти квадратичный невычет по модулю р за полиномиальное время. Наши надежды оправдаются, если считать, что верна до сих пор не доказанная расширенная гипотеза Римана (см., например, [Bund], п. 7.3, Теорема 12 или [КоЫ], п. 5.1 или [Кгап], п. 2.10). Насколько мы сомневаемся в справедливости расширенной гипотезы Римана, настолько является вероятностным алгоритм Шенкса. При построении функции prootJO не будем принимать во внимание эти соображения и просто будем считать, что время работы алгоритма полиномиально. Дальнейшие подробности см. в работе [Cohe], стр. 33 и далее. | Функция: Извлечение квадратного корня из а по модулю р | Синтаксис: jnt prootj (CLINT a_l, CLINT pj, CLINT xj); I Вход: aj, pj (операнды, число pj > 2 - простое) | Выход: х_1 (квадратный корень из а_1 по модулю pj) I Возврат: ^ 0, еслиа_1- квадратичный вычет по модулю pj, -1 в противном случае int prootj (CLINT a J, CLINT pj, CLINT xj) { CLINT bj, qj, t_l, y_l, zj;
глАВА 10. Основные теоретико-числовые функции int r, m; ) : Г if (EQZ_L (p_l) || ISEVEN_L (pj)) { return -1; } Если aj == 0, то результат: 0. if (EQZ_L (aj)) { SETZERO_L (xj); return 0; } Шаг 1. Находим квадратичный невычет. еру J (q_l, pj); dec J (qj); r = twofact_l (qj, qj); cpy_l (zj, twoj); while (jacobij (zj, pj) == 1) { incj (zj); } e* mexpj (zj, qj, zj, pj); Шаг 2. Инициализация рекуррентной последовательности. cpyj (yJ, zj); dec_l (qj);
224 Криптография на Си и C++ в действии /VX-kW'U^i shr_l (q_l); mexpj (aj, qj, xj, pj); msqrj (xj, bj, pj); mmulj (bj, a_l, bj, pj); mmulj (xj, a J, xj, pj); Шаг З. Завершение процедуры; в противном случае находим наименьшее т, для которого z =1 mod p . mod J (bj, pj, qj); while (!equ_l (qj, one_l)) { m = 0; do { (им»!*-'* mm * ++m; msqrj (qj, qj, pj); } while (!equ_l (qj, onej)); if (m == r) { return -1; } Шаг 4. Рекуррентная формула для х, у, z и г. mexp2_l (y_l, (ULONG)(r - m - 1), tj, pj); msqrj (tj, yj, pj); mmulj (xj, tj, xj, pj); mmulj (bj, yj, bj, pj); cpyj (qj, bj); r = m; \%
ГЛАВА 10. Основные теоретико-числовые функции 225 return 0; } С учетом результатов, полученных для модуля р, мы теперь можем извлекать квадратные корни по модулю рк. Для этого рассмотрим сначала сравнение (10.18) x2 = amodp2. Если х\ - решение сравнения х2 = a mod р, то, полагая х := х\ + р • х2, получаем ( 2 mod p , х2 - а = х2 - а + 2рх{х2 + р2х2 = d хл - а ——^———— -р ^,Д,. Д,~ то есть для решения сравнения (10.18) нам нужно найти решение х2 w\ линейного сравнения _ х2 - а _ _, х • 2х1 + —! = 0 mod p . • 0 Г- ■ ^ Продолжая рекурсивно, за конечное число шагов получаем решение сравнения х2 = a mod рк для любого /: G IN. ;г 10.4.3. Квадратные корни по модулю п р . , >• Мы сделали важный шаг - научились извлекать квадратные корни по модулю простого числа - на пути к конечной цели - решению сравнения х2 = a mod n для составного числа п. Справедливости ради отметим, что эта задача не из легких. В принципе, она разрешима, но требует значительных вычислений, объем которых увеличивается экспоненциально с ростом п. Решить это сравнение трудно настолько (в теоретико-сложностном смысле), насколько трудно разложить число п на простые множители. Обе задачи лежат в классе NP (см. стр. 119). Следовательно, извлечение корней по модулю составных чисел связано с задачей, которая на сегодняшний день не разрешима за полиномиальное время. И вряд ли мы сможем решить сравнение быстро для больших п. Тем не менее, если есть два сравнения второй степени: у = a mod r и z = a mods, где числа г us взаимно просты, то можно объединить решения этих сравнений и получить решение сравнения х = a mod rs. В этом нам поможет китайская теорема об остатках: Пусть натуральные числа mh ..., тг попарно взаимно просты (то есть НОД(т1, щ) = / при пц ч^т) и числа ah ..., аг - произвольные целые. Тогда существует решение системы сравнений х =at mod mif причем это решение единственно по модулю произведения т]т2...тг.
226 Криптография на Си и C++ в лействй Хотелось бы уделить немного времени доказательству этой теоре. мы, поскольку именно в доказательстве сокрыто обещанное решение. Положим т := т\тг...тг и m'j := m/nij. Тогда число m'j целое и НОДО?*';, mj) = 1. Из п. 10.2 мы знаем, что существуют целые числа Uj и vj такие, что 1 = т }и} + /w/v,, то есть m'jUj = 1 mod w, для j = 1, ... г, и умеем их вычислять. •1 ■• - '*J". (10.19) Ы\Н ЗШ г : О! Построим сумму Г *о:= Yjm'juJaJ' 7=1 I тогда, в силу сравнения /и'уИ/ = О mod m, для / *j, получаем окончательное решение: г х0 := ^m'jUjuj = щи1а1 = ai mod mi. Предположим, что существуют два решения: xq = я, mod m, и jci = at mod га, имеем х0 = jq mod /?zb или, эквивалентно, разность хо - х\ делится одновременно на все mh то есть на наименьшее общее кратное чисел пц. А так как числа пц попарно взаимно просты, их наименьшее общее кратное есть не что иное как произведение всех этих чисел, и х0 = *i mod m. С помощью китайской теоремы об остатках будем искать решение сравнения х1 = a mod rs, при этом НОД(г, s) = 1, числа г и s - нечетные простые и ни г, ни s не делят а. Пусть уже известны решения сравнений у2 = a mod г и z2 = a mod s. Найдем общее решение сравнений х = у mod г, х = z mod 5 как хо := zur + yvs mod rs, где 1 = иг + vs - линейное представление наибольшего общего делителя чисел г и s. Тогда х2 = a mod r их02=а mod s, а так как НОД(г, s) = 1, то будет верно и сравнение х1 =а mod rs и мы нашли решение сравнения второй степени. Как мы уже говорили, каждое из сравнений по модулю г и s имеет два решения, именно ±У и ±z, тогда, подставляя эти значения в выражение для х0, получаем четыре решения сравнения по модулю rs: (10.20) Xq := zur + yvs mod rs, (10.21) X\ := -zur - yvs mod rs = -Xq mod rs,
ГЛАВА 10. Основные теоретико-числовые функции 227 (10.22) Х2 := -zur + yvs mod rs, (10.23) ~'кТЛ№ хз := zur-yvs mod rs = -x2 mod rs. Таким образом, мы можем свести решение сравнений второй степени вида х = a mod я, ,2_ где число п нечетное, к случаю х = a mod p с простым р. Для этого найдем разложение п = pf1... pf' и вычислим корни по модулям р„ из которых с помощью рекурсивной процедуры п. 10.4.2 находим решения сравнений х2 = a mod pf' . И последний аккорд: по китайской теореме об остатках объединяем эти решения в решение сравнения х2 = a mod п. Функция, которую мы сейчас рассмотрим, находит решение сравнения х2 = a mod n именно таким способом. Единственное ограничение: мы предполагаем, что п= р • q, где числа р и q - нечетные простые. Сначала ищем решения jci и х2 сравнений х2 = a mod p, х2 = a mod q, а затем восстанавливаем из х\ и х2 решения сравнения х2 2= a mod pg рассмотренным выше методом. В качестве квадратного корня из а по модулю pq берем наименьшее из полученных значений. | Функция: Извлечение квадратного корня из а по модулю р • q, где числа р и q - нечетные простые | Синтаксис: int rootj (CLINT a J, CLINT pj, CLINT qj, CLINT xj); I Вход: a_l, pj, qj (операнды, простые числа pj, qj > 2) I Выход: xj (квадратный корень из aj по модулю р_1 * qj) I Возврат: 0, если aj - квадратичный вычет по модулю pj * qj, -1 в противном случае int root J (CLINT aj, CLINT pj, CLINT qj, CLINT xj) { CLINT xOJ, x1J, x2_l, x3_l, xpj, xqj, nj; CLINTD uj, v I:
228 Криптография на Си и C++ в действии dint *xptr_l; int sign_u, sign_v; Вычисляем корни по модулям р_1 и qj с помошью функции proot_l(). При а_1 == 0 результат: 0. if (0 != prootj (aJ, р_1, хр_1) || 0 != prootj (a_l, qj, xq_l)) { return-1; } if (EQZ_L (a_l)) p-H .*' «.\ SETZERO_L (xj); return 0; §РШШШ^^40-:Ж«ШЩ^ li :u :ы Для корректного применения китайской теоремы об остатках, следует учесть знаки чисел u_l и v_l. Для задания этих знаков введем вспомогательные переменные sign_u и signv, значения которых будем вычислять с помошью функции xgcd_l(). Результатом этого шага является корень х0. mul_l (p_l, qj, n_l); xgcdj (p_l, qj, xOJ, u_l, &sign_u, v_l, &sign_v); mulj (uj, pj, uj); mulj (uj, xqj, uj); mulj (vj, qj, vj); mulj (vj, xpj, vj); sign_u = sadd (uj, sign_u, vj, sign_v, xOJ); smod (xOJ, sign_u, nj, x0_l); Теперь находим корни xv x2 и х3. sub_l (nj, xOJ, x1J); msubj (uj, vj, x2J, nj);
ГЛАВА 10. Основные теоретико-числовые функции 229 СИ/ '.ре п 'U;fp subj (n_l, x2J, x3_l); Наименьшее значение берем в качестве результата. xptrj = MIN_L(x0J,x1J); xptrj = MIN_L (xptrj, x2J); xptrj = MIN_L (xptrj, x3J); cpyj (x_l, xptrj); return 0; Теперь мы легко можем реализовать китайскую теорему об остатках, обобщив только что рассмотренную функцию на случай большего числа переменных. Это и сделано в следующем алгоритме, принадлежащем Гарнеру (Garner) (см. [MOV], стр. 612). Преимущество этого алгоритма перед приведенным выше состоит в том, что остатки вычисляются только по модулям ть а не по модулю т = пцт2...тп что значительно сокращает время вычислений. Алгоритм 1 решения системы линейных сравнений х = at mod mh 1 < i < г, где НОД(/я„ mj) = 1 при i Ф] 1. Положить w <— аи х <г- и и i <г- 2. 2. Положить С/ <— 1, j' <r- 1. 3. Вычислить и <— mj~l mod пц (расширенным алгоритмом Евклида; см. стр. 202) и С/ <r— uCi mod пц. 4. Положитьу <—у + 1. Приу < / - 1 вернуться на шаг 3. i-i 5. Положить и <— (at - x)Ct mod пц и х <r- x + wJ~Ja?z7 . 6. Положить i<r-i+ 1. При i< r вернуться на шаг 2. Иначе результат: х Вообще говоря, не очевидно, что этот алгоритм делает именно то, что нужно. Докажем его корректность по индукции. Пусть г = 2, тогда на шаге 5 х = ci\ + ((а2 - ci\)u mod mi)ni\. Сразу видно, что х = а{ mod m{. Чуть менее тривиально х = а\ + (а2 - ai)mi(nii~l mod m2) = а2 mod m2.
230 Криптография на Си и C++ в действии Для выполнения индукционного перехода от г к г + 1 предположим что алгоритм выдает нужный результат хг для некоторого г>2 и добавим еще одно сравнение x = ar+i mod шг+1. Тогда на шаге 5 получаем **•"' х = х + (ягх, - jc)TTw / mod v+i / 7=1 По индукционному предположению, х = хг == a, mod m, для / = 1, \\ г% Кроме того, = хг + ( г г \ (Яг+1--*)ПтгПтуТ1 7=1 7=1 I- = ar+l mod mr+l, что и требовалось доказать. Для практической реализации китайской теоремы об остатках воспользуемся одной очень хорошей функцией, в которой не нужно заранее задавать число сравнений - его можно ввести во время работы программы. Модифицируем процедуру, приведенную выше. При этом мы, увы, теряем возможность вычислять только по модулям ть но зато можем оперировать с переменным числом параметров щ и 1щ системы сравнений при фиксированных затратах памяти. Вот этот алгоритм (см. [Cohe], п. 1.3.3). Алгоритм 2 решения системы линейных сравнений х = ai mod ш„ 1 < i < г, где НОД(ш„ mj) = 1 при i *j 1. Положить i <r- 1, т <— тх и х <— ах. 2. Если / = г, то закончить алгоритм с результатом х. Иначе положить / <— / + 1 и найти расширенным алгоритмом Евклида (см. стр. 199) и и v такие, что 1 = шп + v/и/. 3. Положить х <- umai + vmpc, т <r- mmh х <— х mod m и вернуться на шаг 2. Этот алгоритм становится понятен уже для случая трех сравнений: х = at mod mh i= 1, 2, 3. Для / = 2 получаем на шаге 2 1 = U\11l\ + ViW2> на шаге 3: Х\ = uim\a2 + V\m2a\ mod т\т2. При следующем прохождении цикла для / = 3 обрабатываем параметры а3 и гщ. Получаем на шаге 2 1 = и2т + у2/щ = и2т\т2 + v2m3 I
f ДАВА 10. Основные теоретико-числовые функции 231 и на шаге 3 х2 = и2таъ + v2m3*i mod тпц - = и2т\т2аъ + v2mi,U\m\a2 + v2m^vim2tf 1 mod тхт^т^. Слагаемые и2т\ш2аъ и \21щи\\ща2 уходят, если взять вычет х2 mod mi. Кроме того, v2m3 = v\in2 = 1 mod mi по построению, следовательно, х2 = fli mod mi есть решение первого сравнения. Аналогичными рассуждениями можно показать, что х2 является решением двух других сравнений. Реализуем индуктивный способ построения решения в виде функции chjnrem_l(), позволяющей применять китайскую теорему об остатках с переменным числом сравнений. Строим вектор четной длины указателей на CLINT-объекты яь ти а2ч т2, а3> тз> ••• - коэффициентов системы сравнений х = at mod m,-. Поскольку ее решение имеет длину порядка ^.logm, , при большом числе сравнений или размере параметров может возникнуть переполнение. Такие ошибки придется распознавать и сообщать о них при возвращении значения функции. Функция: Синтаксис: Вход: Выход: Возврат: Применение китайской теоремы об остатках для решения системы линейных сравнений int chinremj (int noofeq, dint **koeff_l, CLINT xj); noofeq (число сравнений) koeffj (вектор указателей на С LI NT-коэффициенты а^ mi сравнений х = ai mod пц, где / = 1, ..., noofeq) х_1 (решение системы сравнений) E_CLINT_OK, если все порядке E_CLINT_OFL в случае переполнения 1, если значение noofeq равно 0 2, если числа пц не попарно взаимно просты int chinremj (unsigned int noofeq, dint** koeffj, CLINT xj) { dint *aij, *mij; CLINT gj, uj, vj, mj; unsigned int i; int sign_u, sign_v, sign_x, err, error = E_CLINT_OK; if (0 == noofeq)
Криптография на Си и C++ в действии { return 1; } Инициализация. Обрабатываем коэффициенты первого сравнения. cpyj (x_l, *(koeffJ++)); cpy_l (m_l, *(koeff_!++)); Если есть еше сравнения, то есть noofeq > 1, то обрабатываем параметры остальных сравнений. Если хотя бы одно из значений mi_l не взаимно просто с предыдущими модулями, входящими в произведение m_l, то функция завершается с кодом ошибки 2. for (i = 1; i < noofeq; i++) { ;: aij = *(koeffJ++); a mij = *(koeff_l++); f-|fH xgcdj (mj, mij, g_l, uj, &sign_u, v_l, &sign_v); jf (!EQONE_L(g_l)) { * I return 2; , ^ • t r } Будем сохранять ошибку переполнения. По завершении функции код ошибки будет храниться в переменной error. err = mulj (u_l, mj, uj); if (E_CLINT_OK== error) { error = err; i }
ГААВА 10. Основные теоретико-числовые функции 233 err = mul_l (u_l, ai_l, uj); if (E_CLINT_OK== error) w av;;£ • - 'iM-Г ..V ) •' • .' error = err; */%f } err = mulj (vj, mij, vj); if (E_CLINT_OK == error) { error = err; } err = mulj (vj, xj, vj); if (E_CLINT_OK == error) -" -ii:^\ «'.a1 { error = err; Побеспокоимся о знаках sign_u и sign_v (или sign_x) переменных uj и vj (или х_1) и снова воспользуемся вспомогательными функциями saddQ и smod(). sign_x = sadd (u_l, sign_u, vj, sign_v, xj); err = mulj (mj, mij, mj); >' v. ..» ;■ -.j . ,<*ы , ш if (E_CLINT_OK == error) { error = err; smod (xj, sign_x, mj, xj); return error; }
234 Криптография на Си и C++ в действии 10.4.4. Квадратичные вычеты в криптографии А вот и обещанные (на стр. 212) примеры применения квадратичных вычетов и квадратных корней в криптографии. Сначала рассмотрим протокол шифрования Рабина, а затем схему аутентификации Фиата-Шамира.7 Безопасность протокола шифрования, опубликованного в 1979 г. Майклом Рабином (Michael Rabin) (см. [Rabi]), основана на сложности задачи извлечения квадратных корней в кольце ЖРЧ. Очень важно, что доказано, что эта задача эквивалентна задаче разложе-. ния на множители (см. также [Кгап], п. 5.6). Этот протокол весьма прост в реализации, поскольку требует лишь возведения в квадрат по модулю п. Генерация ключей для протокола Рабина 1. Участник А генерирует два больших простых числа р ~ q и вычисляет па = р • q. 2. Открытым ключом для А является число па, секретным ключом - пара (р, q). Участник В может послать участнику А сообщение М е Z„, зашифрованное на открытом ключе /и. Протокол Рабина шифрования с открытым ключом 1. Участник В, используя функцию msqr_l() со стр. 92, вычисляет С := М2 mod па и посылает участнику А шифртекст С. 2. Чтобы расшифровать полученное сообщение, А вычисляет четыре квадратных корня М,- из С по модулю па (/= 1, ..., 4) с помощью функции root_l() (см. стр. 227), при этом функция должна выдавать не наименьшее значение квадратного корня, а все четыре значения.8 Один из этих корней и есть исходный открытый текст М. Теперь перед А встает вопрос: какой же из четырех корней М, соответствует открытому тексту ml Если А и В предварительно договорятся о некоторой избыточности сообщения М, скажем, в сообщении М последние г бит должны быть одинаковыми, то у А не будет 7 Основные понятия, необходимые для понимания криптографии с открытым ключом, читатель по-прежнему найдет в главе 16. 8 Не умаляя общности, можно считать, что НОД(М, па) = 1 и, значит, существует четыре различных корня из С. В противном случае отправитель В может разложить число па на множители, вычислив НОД(М, па), что, разумеется, совершенно недопустимо.
ГЛАВА 10. Основные теоретико-числовые функции 235 jf.> *кФ проблем с выбором правильного текста (вероятность того, что одинаковыми будут г бит в одном из трех остальных текстов, пренебрежимо мала). Кроме того, избыточность позволяет предотвратить следующую атаку на протокол Рабина. Если нарушитель X выберет случайное число RtZn и сможет получить от А (не важно под каким предлогом) один из корней /?, сравнения X := R2 mod лд, то с вероятностью у будет выполняться сравнение Я, Ф ±R mod nA . Из соотношений нА= р • q\(R? -R2) = (Я, - tf)(tf, + R) Ф О получаем 1 Ф НОД(# - Rh пд) е {/?, q}, то есть X может вскрыть ключ, разложив на множители число яд (см. [Bres], п. 5.2). Если же открытый "V текст обладает избыточностью, то А всегда может понять, какой из $J. корней соответствует истинному открытому тексту. Максимум, что может сделать А - это открыть нарушителю значение R (при условии, что R имеет нужный формат), совершенно для X бесполезное. ч^- При практическом использовании этого протокола недопустим V, умышленный или случайный доступ к значениям квадратных корней из шифртекста. Еще один пример применения квадратичных вычетов в криптогра- ■■;? /( фии - протокол аутентификации Амоса Фиата (Amos Fiat) и Ади Шамира (Adi Shamir), опубликованный в 1986 г. и специально предназначенный для использования в смарт-картах. Пусть / - последовательность символов, которые содержат информацию, идентифицирующую пользователя А, т - произведение двух больших ■■глп'дь- простых чисел р и q,flZ,n)-)Zm - случайная функция, отображающая произвольные конечные последовательности символов Z и натуральных чисел п в кольцо классов вычетов Жт некоторым непредсказуемым образом. Простые делители р и q числа т известны только удостоверяющему центру. Рассмотрим алгоритм генерации удостоверяющим центром компонентов ключа по информации / и щвдои пока еще не определенному числу к е ИМ. 0;Ш|ОД<.;' .... J ;Ппто > (, ъ. Алгоритм генерации ключей для протокола Фиата-Шамира HtiarNqrov i Вычислить значения v,- =Д/, /) е Жт для некоторого i > к е IN. Н .. -ч ■ 1 f4 2. Выбрать к различных квадратичных вычетов vi{,..., vik из v, и вычислить наименьшие значения 5/9 ...,J/jt квадратных корней из vr\...,vr' вЖт. 3. Сохранить значения / и S; ,..., s; с защитой от несанкциониро- г h 1к ванного доступа (например, на смарт-карте).
236 Криптография на Си и C++ в действии I Для выработки ключей sh можно воспользоваться функциями jacobi_l() и root_l(), а в качестве функции / взять одну из хэщ~ функций из главы 16 (например, RIPEMD-160). Как сказал однажды на конференции Ади Шамир: «Сойдет любая безумная функция». Используя информацию, хранящуюся у удостоверяющего центра на смарт-карте, участник А может быть аутентифицирован участником В. Протокол аутентификации а-ля Фиата-Шамира 1. Участник А отправляет участнику В значения / и /,-, где у = 1, . к. 2. Участник В генерирует значения v,-. = /(/,/,-) е Жт для у = 1, ..., к. Далее для т= 1, ..., t выполняются шаги 3-6 (значение t пока не определено). 3. Участник А выбирает случайное число гхе Жт и отправляет участнику В значение хх = гх. 4. Участник В отправляет участнику А двоичный вектор (^т, > • ■ •»е%к). 5. Участник А отправляет участнику В числа ух := fT J]_ =15/ G %т- 6. Участник В проверяет, что хх := у] Y\, _iv/ • Если А действительно знает числа s,-,..., sik, то на шаге 6 выполняется последовательность равенств в кольце Zm: и, таким образом, участник А может доказать свою подлинность участнику В. Нарушитель, стремящийся узнать информацию об А, может с вероятностью 2~kt угадать вектор (е ,..., ех ), отправляемый участником В на шаге 4, а для этого предусмотрительно послать на шаге 3 участнику В значение хх = rxY\, _ v, • Например, при k = t= 1 вероятность успеха для нарушителя будет равна ■£• То есть значения к и / нужно выбирать так, чтобы вероятность успеха для нарушителя была практически нулевой и чтобы (в зависимости от приложения) получить подходящие значения для: - длины секретного ключа; - объема данных, передаваемых между А и В; - временной сложности, определяемой числом умножений.
ГЛАВА 10. Основные теоретико-числовые функции 237 Подходящие значения параметров приведены в работе [Fiat] для ,. , ,;.,. различных к и t при kt = 72. Итак, безопасность рассмотренного протокола определяется защищенностью значений st., выбором параметров к и t и сложностью задачи разложения: тот, кто сумеет разложить модуль т на множители, сможет вычислить и компоненты sh секретного ключа. Значит, модуль т нужно выбрать так, чтобы его трудно было разложить на множители. И здесь мы снова отсылаем читателя к главе 16, где будет обсуждаться проблема генерации модулей для криптосистемы RS А. Отметим, что в протоколе Фиата - Шамира участник А может проходить аутентификацию сколь угодно часто, не выдавая при этом -j. никакой информации о секретном ключе. Подобные алгоритмы называются доказательством с нулевым разглашением (см., напри- ?г < < мер, [Schn], п. 32.11). 10.5. Проверка на простоту Не буду больше томить Вас неизвестностью - самое большое число Мерсенна, Ml 1213, по- моему, самое большое из известных на сегодня простых чисел, содержит 3375 разрядов, то есть равно примерно Т-281 -% .9 Айзек Азимов, Дополнительное измерение, 1964 Число 26972593-1-простое!!! http://www.utm.edu/research/primes/largest.html (Май 2000)JO Изучение простых чисел и их свойств - одно из старейших направлений в теории чисел и имеет огромное значение для криптографии. Вроде бы безобидное определение: простое число - это натуральное число, большее 1 и делящееся только на само себя и 1, - а сколько оно породило проблем и вопросов. Вот некоторые из них: «Сколько существует простых чисел?», «Как простые числа распределены в множестве натуральных чисел?», «Как можно проверить число на Через Т Азимов обозначает триллион - число 1012, то есть Т-281 ^ равно 1012' ' = 10 ~ 2 В ноябре 2001 г. было найдено 39-е простое число Мерсенна- 213466917 - 1 http://www.mersenne.org - Прим. перев.
238 Криптография на Си и C++ в действии простоту?», «Как можно определить, что натуральное число не является простым (то есть составное)?», «Как разложить составное число на простые множители?». Математики веками пытались их решить, и многие вопросы до сих пор остаются без ответа. 2300 лет назад Евклид доказал, что простых чисел бесконечно много (см., например, [Bund], стр. 5, особенно доказательства: забавное и серьезное, на стр. 39 и 40). Сформулируем еще одно важное утверждение, которое до сих пор подразумевалось по умолчанию: согласно основной теореме арифметики, каждое натуральное число, большее 1, можно разложить в произведение конечного числа простых чисел, причем это разложение единственно с точностью до порядка сомножителей. Простые числа - это своего рода «кирпичики» в здании натуральных чисел. Не выходя за границы множества натуральных чисел и не отвлекаясь на слишком большие числа, мы можем эмпирически ответить на ряд вопросов и получить конкретные результаты. Конечно, эти результаты в значительной степени зависят от имеющихся вычислительных мощностей и эффективности используемых алгоритмов. Посмотрим на опубликованный в Интернете список самых больших простых чисел - размеры чисел поистине впечатляющие (см. таблицу 10.1)! Таблииа 10.1. Самые большие известные простые числа (ланные на май 2001 г.) Простое число 6972593 _ .j 3021377 _ ^ 2976221 _ 1 1398269 _ . 21257787_1 4859465536 + 1 859433 _ 1 ?756839 _ . 667071.2667071-1 104187032768 4-1 Число разрядов 2098960 909526 895932 420921 378632 307140 258716 227832 200815 197192 Автор Hajratwala, Woltman, Kurowski, GIMPS Clarkson, Woltman, Kurowski, GIMPS Spence, Woltman, GIMPS Armengaud, Woltman, GIMPS Slowinski, Gage Scott, Gallot Slowinski, Gage Slowinski, Gage Toplic, Gallot Yves Gallot Год 1999 1998 1997 1996 1996 2000 1994 1992 2000 2000 Самые большие известные простые числа имеют вид 2р-\. Эти простые числа называются числами Мерсенна, по имени Марина Мер- сенна (Marin Mersenne) (1588-1648), открывшего их в процессе поис- i
ГЛАВА 10. Основные теоретико-числовые функции 239 i\ •■& ка совершенных чисел. (Совершенным называется натуральное число, равное сумме всех своих делителей. Например, число 496 является совершенным, так как 496 = 1 + 2 + 4 + 8 + 16 + 31 + 62 + 124 + 248). Для любого делителя t числа р число 2' - 1 является делителем числа 2Р - 1, так как при р = аЪ - 2р _ j = (Г _ 1)(2«(^i) + 2^2) + ... + 1). >^' ■ ■ Следовательно, число 2Р - 1 может быть простым только если чис- , л о р простое. Сам Мерсенн в 1644 г. утверждал (правда, без доказательства), что для р < 257 простыми являются числа вида 2^-1, где ]: ре {2, 3, 5, 7, 13, 17, 19, 31, 67, 127, 257}. Предположение Мерсенна подтвердилось, за исключением р = 67 ир = 257, при которых число 2Р - 1 составное. Были получены результаты и для других показате- j, лей/? (см. [Knut], п. 4.5.4, и [Bund], п. 3.2.12). Отсюда можно, казалось бы, заключить, что чисел Мерсенна бесконечно много, однако это утверждение до сих пор не доказано (см. [Rose], п. 1.2). Интересный обзор нерешенных задач, связанных с простыми числами, читатель найдет в работе [Rose], глава 12. Особым вниманием простые числа стали пользоваться с появлением криптографии с открытым ключом. Как никогда раньше выросла популярность алгоритмической теории чисел и смежных направлений математики. Наибольший интерес вызывают проблемы проверки чисел на простоту и разложения на простые множители. Криптографическая стойкость многих криптоалгоритмов с открытым ключом (прежде всего, системы RSA) зависит от того, насколько трудна задача разложения (в теоретико-сложностном смысле), которая, по крайней мере на сегодняшний день, не разрешима за полиномиальное время. п v То же, в несколько более слабой форме, можно сказать и о распознавании простых чисел: как формально доказать, что данное число простое? Правда, существуют тесты, определяющие (с малой вероятностью ошибки) простоту числа. Более того, если тест о данном числе говорит, что оно составное, то это действительно так. Сомнения в правильности результата теста компенсируются полиномиальным временем работы, а вероятность «неверного положительного ответа», как мы увидим, можно сделать сколь угодно малой, стоит лишь повторить тест нужное число раз. Старинный, но все еще действенный метод получения всех простых чисел, меньших заданного натурального числа N, придуман грече- i ским философом и астрономом Эратосфеном (276-195 до н. э.; см. также [Saga]) и назван в его честь решетом Эратосфена. Обсуждение теоретико-сложностных аспектов криптографии можно найти в [HKW], глава 6, или [Schn], пп. 19.3 и 20.8, см. также многочисленные ссылки в этих работах. Рекомендуем прочитать также сноску на стр. 212.
240 Криптография на Си и C++ в дейст^ Сначала выписываем все натуральные числа от 1 до TV и исклю чаем из этого списка все числа, большие 2 и кратные ему. Зате^ обозначаем за р первое число из оставшихся, большее текущег простого (т.е. больше двух в первый раз) и исключаем из списк 'v*' " числа вида р(р + 2/), / = 0, 1, ... и т. д. Продолжаем процесс до тех пор, пока не найдем простое число, большее VN . После заверще ния процедуры в списке остаются простые числа, меньшие либо • „ . . ,4Г-» - - -, равные N - их мы «поймали в решето». " Вкратце поясним, почему же решето Эратосфена работает так, как * " ~ й надо. Во-первых, простейшая индукция показывает, что число, следующее за простым и оставшееся в списке, само является простым поскольку иначе у него должен быть маленький простой делитель и, следовательно, это число мы должны были отбросить раньше. Поскольку мы исключаем только составные числа, мы не теряем ни одного простого числа. Во-вторых, достаточно исключить только числа, кратные тем простым /?, которые меньше либо равны V N , так как если Т - наименьший собственный делитель числа N, то Т< Если бы в списке осталось составное число п < VN , то наименьший простой 5 делитель р числа п должен был бы удовлетворять неравенству р < vw < <Jn и мы должны были отбросить п как кратное р, а это противоречит нашему предположению. Теперь посмотрим, как можно реализовать решето, а для этого нам нужен программируемый алгоритм. Но сначала несколько замечаний. Поскольку, кроме 2, других четных простых чисел нет, будем проверять на простоту только нечетные числа. Вместо того чтобы выписывать нечетные числа, составим последовательность/, где 1 < i<[_(N- 1)/2_], соот- г { ' ветствующую простоте чисел 2/ + 1. Далее, пусть переменная р со- 1 держит текущее значение 2/ + 1 элемента (воображаемого) списка " нечетных чисел, а переменная s удовлетворяет соотношению 2s + 1 =р2 = (2/ + I)2, то есть s = 2/2 + 2/. Теперь можно и сформулировать алгоритм (см. [Knut], п. 4.5.4, упражнение 8). Алгоритм поиска всех простых чисел, меньших либо равных натуральному числу N (решето Эратосфена) х, , mC,ii( ,, 1Г у> _ 1. Положить L <-L(N- l)/2j и B<-\yJ~N/2 |. Для 1 < / < L положить , ,\iK.i fi<— 1. Положить /<— 1,р <— 3 ия^-4. v f, 2. Если/ = 0, то перейти на шаг 4. Иначе результат: р и положить к <— s. 3. При к < L положить/* f-О, к <г-к + р и повторить этот шаг. 4. Если / < В, то положить / <— i + 1, s <r- s + 2p, p <— p + 2 и вернуться на шаг 2. Иначе закончить алгоритм.
ГЛАВА 10. Основные теоретико-числовые функции 241 На основе этого алгоритма напишем программу, возвращающую в качестве результата указатель на список значений типа ULONG, содержащий все простые числа, меньшие заданной границы, в порядке возрастания. Первый элемент списка - это число всех найденных простых чисел. функция: Генератор простых чисел (решето Эратосфена) Синтаксис: ULONG * genprimes (ULONG N); Вход: N (верхняя граница поиска) Возврат: Указатель на вектор значений типа ULONG, содержащий простые числа, меньшие либо равные N. (На нулевой позиции - число найденных простых чисел). NULL при ошибке процедуры mallocQ. ULONG * genprimes (ULONG N) { ULONG i, k, p, s, B, L, count; char *f; ULONG 'primes; Шаг 1. Задание начальных значений переменных. Для вычисления целой части квадратного корня из числа типа ULONG используется вспомогательная функция ul_iroot(), см. соответствующую процедуру в п. 10.3. Составные числа помечаем с помощью вектора f. В = (1 +ul_iroot(N))»1; L = N » 1; if (((N & 1) == 0) && (N > 0)) { --L; if ((f = (char *) malloc ((sizej) L+1)) == NULL) { return (ULONG *) NULL; }
242 Криптография на Си и C++ в лействии for(i = 1;i<=L;i++) { f[i] = 1; } Р = 3; s = 4; С. На шагах 2, 3, 4 собственно и реализуем решето. Переменная i соответствует численному значению числа 2i + 1. for (i = 1; i <= В; i++) { if (ВД) . { for (k = s; к <= L; к += p) { f[k] = 0; } } 1 s += p + p + 2; p+=2; } r '\ Теперь определяем число найденных простых чисел и выделяем для переменных типа ULONG поле соответствующего размера. for (count = i = 1; j <= L; i++) { count += f[i]; if ((primes = (ULONG*)malloc ((size_t)(count+1), sizeof (ULONG))) == NULL)
ГЛАВА 10. Основные теоретико-числовые функции 243 О щ return (ULONG*)NULL; } Оцениваем поле f[]; все числа вила 2i + 1, помеченные как простые, храним в поле primes. Если N £ 2, то учитываем и число 2. for (count = i = 1; i <= L; i++) { if (f[i]) { primes[++count] = (i « 1) + 1; } - u if (N < 2) pnmes[0] = 0; } - else • ., { primes[0] = count; primes[1] = 2; } ', ...'"" - free(f); ' , "... return primes; } Чтобы определить, является ли число п составным, достаточно применить к нему метод пробного деления: проверить, делится ли п на все простые числа, меньшие либо равные v л (их можно найти с помощью решета Эратосфена). Если п не делится ни на одно из этих чисел, то оно простое. Однако этот метод непрактичен: число простых чисел быстро растет с ростом п. А именно, справедлива теорема о простых числах, сформулированная А. М. Лежандром, согласно которой число п(х) простых чисел р, где 2<р<х, асимптотически стремится к х/\пх при х-><*> (см., например, [Rose],
*ш ^ 244 Криптография на Си и C++ в действии глава 12) 12. Дадим некоторые оценки для числа простых чисел меньших заданного х. В таблице 10.2 приведены и истинные значения числа п(х) простых чисел, меньших либо равных *, и sv^vm* аппроксимация х/\п х. В последней клетке таблицы стоит знак вопроса - может быть, читатель сам ее заполнит? X х/ln х п(х) 102 22 25 104 1 086 1 229 108 5 428 681 5 761 455 1016 271 434 051 189 532 279 238 341 033 925 1018 24 127 471 216 847 323 24 739 954 287 740 860 10100 4- Ю97 ? Таблииа 10.2. Число простых чисел лля различных значений х Сложность метода пробного деления растет почти по экспоненте с ростом х. Следовательно, для проверки простоты больших чисел этот метод совершенно не применим. Позже мы увидим, что метод пробного деления играет важную вспомогательную роль в других тестах. В принципе, нам нужен такой тест, который проверял бы число на простоту, не выдавая никакой информации о его делителях. В этом нам поможет хотя бы малая теорема Ферма, согласно которой для простого числа р и всех чисел я, взаимно простых с р, справедливо сравнение ар~х = 1 mod/? (см. стр. 198). На этом факте основан тест Ферма: если найдется число а, для которого выполняется либо НОД(я, п) Ф 1, либо НОД(я, п) = 1 и 1 Ф an~{ mod /г, то число п составное. Для возведения в степень а"~{ = 1 mod n требуется 0(log n) операций процессора. Опыт показывает, что уже после нескольких попыток составное число будет распознано. Однако тест Ферма не является универсальным и «пропускает» некоторые числа. Посмотрим, какие именно. Следует признать, что утверждение, обратное к малой теореме Ферма, вообще говоря, неверно: число /г, для которого НОД(я, п) = 1 и ап~х = 1 mod n, где 1 < а < п - 1, не обязательно будет простым. Существуют составные числа /z, которые проходят тест Ферма для любых а, взаимно простых с п. Это числа Кармайкла, названные по Теорема о простых числах была доказана независимо в 1896 г. Жаком Адамаром и Шарлем-ЖаноМ де ла Балле Пуссеном (см. [Bund], п. 7.3). (В 1849-1852 гг. аналогичные утверждения были сформулированы П. Л. Чебышёвым; в частности, Чебышёв впервые доказал, что число flCvJ удовлетворяет двойному неравенству 0,921-^ < п(х) < 1,06-^ . - Прим. перев.)
ГЛАВА 10. Основные теоретико-числовые функции 245 имени открывшего их Роберта Дениэла Кармайкла (Robert Daniel Carmichael, 1879-1967). Вот первые три из этих загадочных чисел: 561 = 3- 11- 17,1105 = 5- 13- 17,1729 = 7- 13- 19. jt Любое число Кармайкла представляет собой произведение не менее трех различных простых чисел (см. [КоЫ], глава 5). Лишь в начале {*: ч Wfc ; 1990-х гг. было доказано, что чисел Кармайкла бесконечно много ы &;\ш^;> (см. [Bund], п. 2.3). *х>,,: - Относительная частота, с которой встречаются числа, меньшие п и взаимно простые с п, задается формулой (10.24) 1 Ф(л) /i-l I /л где ф(/г) - функция Эйлера (см. стр. 198), то есть доля чисел, не взаимно простых с п, близка к 0 при больших п. Следовательно, в большинстве случаев, чтобы распознать число Кармайкла, приходится много раз проходить тест Ферма. Заставляя а пробегать весь диапазон 2 < а < п - 1, мы в конце концов найдем наименьший простой делитель числа п, равный а. Помимо чисел Кармайкла существуют и другие нечетные составные числа п, для которых найдутся натуральные а такие, что НОД(я, п) = 1 и ап~1 = 1 mod п. Эти числа называются псевдопростыми по основанию а. Справедливости ради отметим, что лишь немногие числа являются псевдопростыми по основаниям 2 и 3, а в интервале от 1 до 25 • 109 существует всего 1770 целых чисел, псевдопростых по основаниям 2, 3, 5 и 7 одновременно (см. [Rose], п. 3.4). Но факт остается фактом - не существует общей оценки для числа решений сравнения Ферма для составных чисел. Таким образом, к недостаткам теста Ферма можно отнести, во-первых, сомнение в том, действительно ли число, прошедшее тест, является про- [ъ ^'V стым, а во-вторых, невозможность оценить число проходов теста для распознавания составного числа. П Ш' ■Ж№ (10.25) Этих недостатков лишен следующий тест, основанный на критерии Эйлера (см. п. 10.4.1): если/? нечетное простое, то для всех чисел а, не кратных р, выполняется сравнение а(р~{)/2 - ( \ а mod p , где (-7)= ±1 mod p означает символ Лежандра (Якоби). По аналогии с малой теоремой Ферма, критерий проверки на простоту получаем из следующего утверждения: если для натурального числа п существует целое а такое, что НОД(я, п) = 1 и a(n~l)/2 = (f)mod n , то число /г, вероятно, простое. Временная сложность соответствующего теста совпадает со сложностью теста Ферма и равна 0(\og3n).
I 246 Криптография на Си и C++ в действий Опять же, как и для теста Ферма, существуют составные числа п удовлетворяющие критерию Эйлера для некоторого а. Эти числа называются эйлеровыми псевдопростыми по основанию а. Например число л = 91 = 7 • 13 эйлерово псевдопростое по основаниям 9 и ю' так как 945 = (&)г 1 mod 91 и 1045 = (f )= ~1 mod 91. 13 Эйлерово псевдопростое по основанию а всегда является псевдопростым по тому же основанию (см. стр. 245), так как, возводя почленно в квадрат сравнение a{n~l)'2 = (^-jmod п , получаем ап~1 = 1 mod п. К счастью, для критерия Эйлера не существует чисел, аналогичных числам Кармайкла, а следующие наблюдения, отмеченные Р. Соло- вэем (R. Solovay) и В. Штрассеном (V. Strassen), позволяют значительно сузить круг составных чисел, удовлетворяющих критерию Эйлера. (а) Для составного числа п число целых чисел а, взаимно простых с «, для которых a{n~l)l2 =(f)mod«, не превышает уФОО (см. [КоЫ], п. 2.2, упражнение 21). Отсюда следует, что: (б) Вероятность для составного п и случайно выбранных к чисел аь ..., ак, взаимно простых с п, получить a{rn~l)l2 = (-^-jmod n , где 1 < г < п, не превышает 2~к. <}Ь"~ Теперь мы можем реализовать критерий Эйлера в виде вероятност- 4 '• ного теста, где термин «вероятностный» означает, что результат '• ": «число п не простое» является безусловным, а говорить о том, что п является простым мы можем лишь с определенной вероятностью ошибки. Вероятностный алгоритм проверки натурального числа п на простоту (тест Соловэя-Штрассена) , .^ , 1. Выбрать случайное число 2 <а <п- 1 такое, что НОД(я, п) = 1- 2. Если a{n~l)l2 =(^)modft, то результат: «Число п вероятно простое»; иначе результат: «Число п составное». Возведение в степень и вычисление символа Якоби выполняется за время 0(log3n). Многократно повторяя этот тест, можно уменьшить вероятность ошибки в смысле свойства (б). Например, при к = ^ вероятность ошибки пренебрежимо мала - меньше, чем 2 « ДО • Как отмечает Д. Кнут, это меньше, чем случайная аппаратна ошибка, вызванная, например, альфа-частицей, попавшей в npou^c- сор или память компьютера и изменившей значение бита. J В кольце Z9i элемент 3 имеет порядок 9, а элемент 6 — порядок 10, Поэтому 945 s 93 15 г 1 mod 91 и 1045 г 106'7+3 а Ю3 =-1 mod 91. поэтому 93=106 =1 mod9b
ГЛАВА 10. Основные теоретико-числовые функции ^ 247 Казалось бы, мы должны быть довольны этим тестом: мы можем контролировать вероятность ошибки и у нас есть эффективные алгоритмы для выполнения всех необходимых вычислений. Однако существует ряд результатов, позволяющих получить более мощный тест. Чтобы лучше понять суть наиболее широко используемых на сегодняшний день тестов, проведем некоторые рассуждения. Предположим, что число п простое. Тогда по малой теореме Ферма для всех целых чисел а, не кратных п, имеем: ап~1 = 1 mod п. Квад- ' v: ратный корень из an~l mod n может принимать лишь значения 1 и -1, поскольку это единственные решения сравнения х2 = 1 mod n (см. п. 10.4.1). Вычислим последовательно один за другим квадратные корни a(n~l)/2 mod п, я("-1)/4 mod л, ..., a(n~l)/2' mod n, пока не получим нечетное число (п - 1)/2г. И если на некотором шаге мы получим вычет, не равный 1, то он должен быть равен -1 (иначе п не может быть простым, что противоречит нашему предположению). Если же квадратный корень, предшествующий 1, будет равен -1, то мы можем по-прежнему верить, что число п яв- ^ '• ',■ ляется простым. Составные числа п, обладающие таким свойством, называются сильными псевдопростыми по основанию а. Сильное псевдопростое число по основанию а всегда является эйлеровым ' ... псевдопростым по тому же основанию (см. [КоЫ], глава 5). 1 .!*у) i Сведем полученные результаты в следующий вероятностный тест. Из соображений эффективности сначала вычислим Ъ = а{п~Х)'2 mod n с *' г ш нечетным показателем (п - 1)/2, и если Ь Ф 1, будем возводить Ъ в квадрат, пока либо не получим ±1, либо не достигнем а{п~{)12 mod п. , \fh Во втором случае либо Ъ должно быть равно -1, либо число п со- Ht ставное. Укороченный вариант алгоритма, без выполнения послед- и*> него возведения в квадрат, взят из книги [Cohe], п. 8.2. Вероятностный алгоритм проверки нечетного числа п > 1 на простоту (тест Миллера-Рабина) 1. Определить q Ht, где п - 1 = 2lq, число q нечетное. 2. Выбрать случайное число а из интервала 1 < а < п. Положить е <— 0, Ъ <r- a1 mod л, При Ъ = 1 завершить алгоритм с результатом «Число п вероятно простое». 3. Пока Ъ Ф 1 mod п и е < t - 1, вычислять b<r-b2 mod n и е <г- е + 1. Если теперь Ъ Ф п - 1, то результат: «Число п составное»; иначе результат: «Число п вероятно простое». Время возведения в степень равно <9(log3/i), поэтому сложность теста Миллера-Рабина (или, короче, MP-теста) та же, что и сложность теста Соловэя-Штрассена.
T FF 248 Криптография на Си и C++ в действии Существование сильных псевдопростых чисел подразумевает, что лишь результат «Число п составное» теста Миллера-Рабина является безусловным. Число 91, представленное выше как эйлерово псев* допростое по основанию 9, является также - вновь по основанию 9 - сильным псевдопростым. Вот еще сильные псевдопростые числа: 2152302898747 = 6763 • 10627 • 29947, 3474749660383 = 1303 • 16927 • 157543. Кроме них до 1013 не существует больше чисел, псевдопростых по основаниям 2, 3, 5, 7 и 11 одновременно (см. [Rose], п. 3.4). К счастью, оснований, по которым псевдопростое число является таковым, не больше, чем само это число. М. Рабин доказал, что число оснований я, 2 < а < п, по которым данное составное число п является псевдопростым, гораздо меньше, чем л/4 (см. [Knut], п. 4.5.4, упражнение 22, и [КоЫ], глава 5). Отсюда получаем, что при /:-кратном повторении теста для случайно выбранных оснований а{, ..., ак сильное псевдопростое число будет распознано как простое с вероятностью, меньшей чем 4~*. Следовательно, при том же числе операций тест Миллера-Рабина всегда более предпочтителен, чем тест Соловэя-Штрассена, для которого вероятность ошибки при к повторах равна 2~к. На практике тест Миллера-Рабина ведет себя гораздо лучше, поскольку реальная вероятность ошибки в большинстве случаев •■;, .;• ; значительно ниже, чем это гарантирует теорема Рабина (см. [Schn], п. 11.5). Прежде чем перейти к реализации теста Миллера-Рабина, укажем два способа улучшения алгоритма. Во-первых, разумнее сначала применить к тестируемому числу метод пробного деления, то есть проверить, не делится ли это число на маленькие простые числа. Если делитель будет найден, то нечего и применять тест Миллера-Рабина. Но тогда сразу встает вопрос: 1 сколько нужно таких маленьких простых чисел, прежде чем мы сможем применить MP-тест? Воспользуемся рекомендациями А. К. Ленстры: наибольший эффект достигается, если проверить делимость на 303 простых числа, меньших 2000 (см. [Schn], п. Неоткуда взялись такие цифры, становится ясно, если заметить, что ?! <, относительная частота появления нечетных чисел, которые н кратны простым числам, меньшим п, примерно равна 1,12/1п' Проверяя делимость на простые числа, меньшие 2000, мы отбра*| сываем 85% составных чисел без всякого теста Миллера-Рабина,| а его используем лишь для проверки оставшейся доли чисел. Проверка делимости на маленькое простое число требует лиШЧ 0(\п п) элементарных операций. Для этого в методе пробного Д^е1 ния будем использовать специальную процедуру, особенно эфФеК1 тивную при делении на маленькие числа.
f ДАВА 10. Основные теоретико-числовые функции 249 Реализуем метод пробного деления в виде функции sieve_l(), которая, в свою очередь, использует простые числа, меньшие 65536, для хранения которых выделим поле smallprimes[NOOFSMALLPR!MES]. Простые числа будем хранить в виде разностей, то есть под каждое простое число требуется лишь один байт памяти. Ограниченный доступ к этим числам не создает особых проблем, поскольку мы рассматриваем эти числа в естественном порядке. Особо следует выделить случай, когда само тестируемое число является маленьким простым и содержится в таблице. Во-вторых, в тесте Миллера-Рабина будем использовать не случайные основания, а маленькие простые числа 2, 3, 5, 7, 11, ... < В. Это значительно ускоряет работу функции возведения в степень (см. главу 6) и, как показывает опыт, ничуть не ухудшает результаты теста. И вот, наконец, метод пробного деления. Процедура деления на маленькие числа реализована в виде функции divj(). Функция: Синтаксис: Вход: Возврат: Метод пробного деления USHORTsieve_l(CLiNT aj, unsigned no_of_smallprimes); а_1 (тестируемое число) no_of_smallprimes (число простых чисел, на которые мы делим, без учета числа 2) Простой делитель, если таковой найден 1, если тестируемое число само является простым 0, если делитель не найден USHORT sievej (CLINT a_l, unsigned int no_of_smallprimes) { dint *aptr_J; USHORT bv, rv, qv; ULONG rdach; unsigned int i = 1 ; Аля полноты картины сначала проверяем, не является ли a_l кратным 2. Если a_l равно 2, то возвращаем 1; если же а_1 четное, большее 2, то возвращаем 2 в качестве делителя. if (ISEVEN_L (aj)) { if (equj (aj, two J)) (кЛ. I
250 Криптография на Си и C++ в действии { return 1; } else { return 2; } } bv = 2; do { Определяем простые делители, последовательно суммируя числа из smallprimes[], сумму записываем в bv. Первое простое число, проверяемое в качестве делителя, - это 3. Для деления на число типа USHORT используем код из соответствующей быстрой программы (см. п. 4.3). rv = 0; bv += smallpnmes[i]; for (aptrj = MSDPTFLL (aj); aptrj >= LSDPTFLL (aj); aptrj--) { qv = (USHORT)((rdach = ((((ULONG)rv) « BITPERDGT) + (ULONG)*aptrJ)) / bv); rv = (USHORT)(rdach - (ULONG)bv * (ULONG)qv); } } while (rv != 0 && ++i <= no_of_smallprimes); Если найден несобственный делитель (rv == 0 и bv Ф aj; иначе aj само простое!), то он и будет результатом. Если a_l само является простым, то результат: 1; в противном случае результат: 0. if (0 == rv) { if (DIGITS_L (aj) == 1 && *LSDPTR_L (aj) == bv) { ■1 )■ о Щ
ГЛАВА 10. Основные теоретико-числовые функции 251 ■mi л. bv = 1; } /* else: Результат в bv является простым делителем числа a_l */ } else /* Делитель числа a_l не найден */ { bv = 0; } return bv; С помощью функции sieveJO можно находить простые делители, меньшие 65536, объектов типа CLINT. Для этого в flint.h имеется макрос SFACTOR_L(n_l), вызывающий функцию sieve_l(nj, NOOFSMALLPRIMES), которая, в свою очередь, проверяет, делится ли n_l на простые числа из базы smallprimes[]. Макрос SFAC- TOR_L(n_l) возвращает то же значение, что и функция sievej. Многократно вызывая макрос SFACTOR_L(n_l) и тем самым последовательно определяя простые делители, мы можем найти полное разложение чисел, меньших 232, то есть представимых стандартными целочисленными типами. И вот уже prime_l() - вполне созревшая функция, объединяющая в себе метод пробного деления и тест Миллера-Рабина. Для придания ей большей гибкости будем рассматривать число простых делителей в предварительном пробном делении и число проходов теста Миллера-Рабина как параметры. В прикладных задачах для простоты можно использовать макрос ISPRIME_L(CLINT n_l), который, в свою очередь, вызывает функцию prime_l() с наперед заданными параметрами. Возведение в степень будем осуществлять с помощью функции wmexpm_l(), сочетающей в себе всю прелесть приведения по Монтгомери и малых оснований степени (см. главу 6). Функция: Вероятностный тест Миллера-Рабина с использованием метода пробного деления Синтаксис: int primej (CLINT n_l, unsigned no_of_smallprimes, unsigned iterations); Х°Д« nj (тестируемое число) no_of_smallprimes (число простых чисел в методе пробного деления) iterations (число итераций теста) в°зврат: 1, если тестируемое число «вероятно» простое О, если тестируемое число составное или равно 1
i 252 Криптография на Си и C++ в действии int primej (CLINT n_l, unsigned int no_of_smallprimes, unsigned int iterations) { CLINT d_l, xj, qj; i: ' USHORTiJ, k, p; i int isprime = 1; 1 if (EQONE_L (n_l)) { return 0; } { 1 Теперь выполняем пробное деление. Если делитель найден, то функция завершается с результатом 0. Если функция sieveJO выдала 1 (это означает, что число п_1 простое), то функция завершается с результатом 1. В противном случае выполняем тест Миллера-Рабина. k = sievej (nj, no_of_smallprimes); if(1==k) { return 1; if (1 < k) { return 0; i else { / ] 1 Шаг 1. Используя функцию twofactJO, находим разложение ] f I n - 1 = 2kq, где число q нечетное. Значение n - 1 записываем в dj.
ГЛАВА 10. Основные теоретико-числовые функции 253 [ Ы ! cpy_l (d_l, n_l); dec J (d_l); k = (USHORT)twofactJ (d_l, qj); P = 2; i = i; ispn'me = 1; do { Шаг 2. Из прирашений, хранящихся в поле smallprimes[], формируем основания р. Для возведения в степень используем функцию Монтгомери wmexpmj, поскольку основание всегда будет иметь тип USHORT и, кроме того, после предварительной проверки методом пробного деления числа n_l, всегда будет нечетным. Если в результате степень (x_l) равна 1, то переходим к следующей итерации. :\0' р += smallprimes[i++]; wmexpmj (p, qj, x_l, n_ if(!EQONE_L(x_l)) { j = 0; m hL Шаг 3. Пока x_l отлично от ±1 и пока число итераций не превышает к - 1, выполняем возведение в квадрат. while (!EQONE_L (x_l) && !equ_l (xj, d_l) && ++j < k) { msqrj (xj, xj, nj); \ if (!equ_l (xj, d_l)) { isprime = 0; ~ }
254 Криптография на Си и C++ в действии while ((--iterations > 0) && isprime); return isprime; } } Открытым остается вопрос о том, сколько итераций теста Миллера- Рабина следует провести, чтобы получить достоверный результат. Рекомендации самые разные: [Gord] и [Schn] считают, что достаточно пяти итераций (для криптографических приложений), алгоритм в [Cohe] включает 20 итераций. Д. Кнут [Knut] говорит о том, что при 25 итерациях вероятность ошибочно определить число как простое будет меньше, чем 10~6 для миллиарда «кандидатов», хотя, вообще-то, он не настаивает на числе 25, а вместо этого задает философский вопрос: «А так ли уж нужно строгое доказательство простоты?». 14 В том случае, если нам нужен утвердительный ответ, можно воспользоваться тестом APRCL (L. Adleman, С. Pomerance, R. Rumely, H.Cohen, А. К. Lenstra), опубликованным в 1981 г. X. Ризель (Н. Riesel) назвал этот тест прорывом, доказавшим, что быстрые универсальные достоверные алгоритмы проверки на простоту действительно существуют (см. [Ries], стр. 131). Этот тест распознает простоту числа п за время 0((ln/i)cln,nln") для некоторой подходящей константы С. Поскольку на практике показатель In In In n ведет себя как константа, этот тест можно считать полиномиальным. Теперь можно проверять на простоту целые числа длиной в несколько сотен десятичных знаков за такое время, которое раньше могли показать только вероятностные тесты. 15 Алгоритм, использующий аналог малой теоремы Ферма для более сложных алгебраических структур, довольно сложен теоретически и труден в pea- В статье «Генерация вероятно простых случайных чисел» (P. Beauchemin, G. Brassard, С. Crepeau» С. Goutier и С. Pomerance, Journal of Cryptology, Vol. 1, No. I, 1988) говорится о том, что утвер' ждение Д. Кнута верно лишь потому, что вероятность ошибки для большинства составных ч сел гораздо меньше Уа. В противном случае оценка, данная Кнутом, будет значительно больШ » чем 10 Коэн (Cohen) в этой связи замечает, что, хотя практический вариант алгоритма APRCL также ве' роятностный, существует и менее практичная, детерминированная его версия (см. [Cohe], глава 9)- I
[ ГЛАВА 10. Основные теоретико-числовые функции 255 лизации. Более подробную информацию см. в [Cohe], глава 9, в [Ries] или в указанной выше оригинальной статье. Читатель может спросить, получим ли мы достоверно простое число, применяя тест Миллера-Рабина для большого числа оснований. Согласно результату Г. Миллера (G. Miller), нечетное натуральное число п является простым тогда и только тогда, когда тест Миллера-Рабина определяет его как простое для всех оснований а таких, что 1 <а<С• 1п2л (в [Kobl], п. 5.2, указано, что С = 2), в предположении, что верна расширенная гипотеза Римана (см. стр. 222). При таких условиях тест Миллера-Рабина является детерминированным и полиномиальным и за 2,5 • 105 итераций дает достоверный результат для чисел длины 512 бит. Если на каждую итерацию отводится 10"1 секунд (время возведения в степень на быстрых ПК; см. Приложение D), тогда достоверный тест займет около семи часов. Однако, принимая во внимание, что этот тест основан на недоказанной гипотезе, а также учитывая довольно длительные вычисления, можно ожидать, что такой теоретический результат не устроит ни «чистых» математиков, ни программистов-прагматиков, любящих быстрые программы. Генри Коэн, отвечая на процитированный выше вопрос Д. Кнута, был категоричен ([Cohe], п. 8.2): «Все же проверка на простоту требует строгих математических доказательств».
ГЛАВА П. Большие случайные числа Математика полна псевдослучайности, которой вполне хватит всем изобретателям-мечтателям на все времена. Д.Р. Хофштадтер, Гедель, Эшер, Бах Последовательности «случайных» чисел широко используются в статистических процедурах, в вычислительной математике, в физике, а также в теоретико-числовых приложениях, когда нужно либо заменить ими статистические наблюдения, либо автоматизировать процесс ввода каких-то переменных величин. Случайные числа могут пригодиться: • если мы хотим выбрать несколько случайных элементов из большого множества; • в криптографии для генерации ключей и работы защищенных протоколов; */ в качестве начальных значений при генерации простых чисел; %/ для тестирования компьютерных программ (к этой теме мы еще вернемся); |/ для развлечения и много для чего еще. При компьютерном моделировании естественных процессов случайными числами можно представлять измеряемые величины {методы Монте-Карло). Случайные числа полезны и в том случае, если нам нужны произвольные, случайным образом выбранные, числа. Прежде чем приступить в этой главе к разработке каких-либо функций генерации больших случайных чисел, которые нам понадобятся, в частности, для криптографических приложений, проведем некоторую методологическую подготовку. Существует множество способов получения случайных чисел, однако мы сразу условимся разделять истинно случайные числа, возникающие в результате случайных экспериментов, и псевдослучайные числа, выработанные алгоритмически. Истинно случайные числа можно получить, подбрасывая монету или кость, вращая («честное») колесо рулетки, наблюдая процесс радиоактивного распада на специальном измерительном оборудовании. Напротив, псевдослучайные числа вырабатываются алгоритмами, с помощью генераторов псевдослучайных чисел, которые, в свою очередь, являются детерминированными и, вследствие этого, предсказуемыми и воспроизводимыми. Таким образом, псевдослучайные числа не являются случайными в строгом смысле слова. Этим обстоятельством, однако, можно пренебречь, если у нас есть алгоритмы, производящие «высококачественные» случайные числа. Поясним, что мы понимаем под этим словом. 1-1697
258 Криптография на Си и C++ в действии Прежде всего, обратим внимание читателя на то, что бессмысленно говорить о «случайности» какого-то одного числа. Математические критерии случайности всегда применяются к последовательности чисел. Д. Кнут говорит о последовательности независимых случайных чисел с определенным законом распределения, в которой каждый элемент вырабатывается случайно и независимо от всех других членов последовательности и принимает значение из некоторого диапазона с определенной вероятностью (см. [Knut], п. 3.1). Слова «случайно» и «независимо» используются здесь, чтобы подчеркнуть, что характер и способ взаимодействия событий, определяющих выбор конкретного числа, слишком сложен, чтобы распознать его статистическими или какими-либо другими тестами. Теоретически достичь этого идеала детерминированными процедурами невозможно. Цель же многочисленных алгоритмических средств генерации чисел - как можно ближе приблизиться к этому идеалу. Параллельно разрабатываются теоретические и эмпирические тесты для распознавания характера и структуры последовательностей псевдослучайных чисел и, следовательно, для оценки качества алгоритмов генерации этих последовательностей. Не будем слишком этим увлекаться: теория здесь слишком глубока и сложна. Хороший обзор на эту тему желающие смогут найти в книге [Knut], а исчерпывающие теоретические оценки генераторов случайных чисел - в работе [Nied]. Ряд прагматических идей по тестированию последовательностей случайных чисел есть в [FIPS]. Из множества существующих способов генерации псевдослучайных •■■* чисел (для краткости будем иногда опускать слово «псевдо» и гово- ( ' рить просто «случайные числа», «случайные последовательности» и «генераторы случайных чисел») уделим сначала немного времени ••; проверенному и часто используемому методу генерации линейных конгруэнтных последовательностей. По заданному начальному значению Х0 элементы последовательности определяются из линейного рекуррентного соотношения: (11-V Xi+{ = (Xta + b) mod m. Эта процедура была предложена Д. Лемером (D. Lehmer) в 1951 году и с тех пор завоевала большую популярность. Несмотря на кажущуюся простоту, линейные конгруэнтные последовательности обладают хорошими свойствами случайности. Их качество, как. нетрудно догадаться, зависит от выбора параметров а, Ъ и т. В книге [Knut] показано, что линейная конгруэнтная последовательность с тщательно подобранными параметрами проходит испытания статистическими тестами «на ура», однако случайный выбор параметров почти всегда приводит к плачевным результатам. Мораль сей басни такова: при выборе параметров будьте осторожны! Выбор в качестве т степени двойки обладает очевидным преимуществом: вычислять вычет по модулю т можно с помощью мате-
ГЛАВА 11. Большие случайные числа 259 Рисунок 11.1. Повеление псевлослуча иной послелова- тельности матической операции AND. Но, как всегда, есть и недостаток - младшие двоичные разряды генерируемых чисел характеризуются гораздо меньшей случайностью, чем старшие, а значит, надо быть очень аккуратным при работе с такими числами. Да и вообще, числа, полученные из линейной конгруэнтной последовательности приведением по модулю простого делителя числа т, проявляют весьма посредственные свойства случайности, поэтому следует рассмотреть возможность выбора в качестве т простого числа, так как в этом случае любые двоичные разряды ничуть не хуже, чем любые другие. Выбор чисел а и т влияет на периодичность последовательности: поскольку элементы последовательности могут принимать конечное число, а именно т, различных значений, последовательность начнет повторяться самое позднее на (ш + 1)-м элементе. То есть, эта последовательность периодическая. (Говорят также, что последовательность входит в цикл). Точка входа в цикл - не обязательно начальное значение Х0, это может быть и некоторое более позднее значение ХИ. Числа Х0, Хь Х2, ..., Х^\. образуют «хвост» последовательности. Поведение такой последовательности схематично изображено на рис. 11.1. «Хвост» Цикл hi •пг Поскольку повторение чисел с коротким периодом совершенно не подходит ни под какие критерии, мы должны приложить все усилия, чтобы максимально увеличить длину цикла или даже построить генератор, вырабатывающий только последовательности с максимальной длиной цикла. Сформулируем критерий, позволяющий создавать именно такие линейные конгруэнтные последовательности с параметрами a, b и ш. Итак, должны выполняться следующие условия: (а) НОД(6,/и)=1. (б) Если р | т, то р | (а - 1) для любого простого числа р. (в) Если 4 | т, то 4 | (а - 1). Доказательство и дополнительные подробности см. в [Knut], п. 3.2.1.2. Стандарт ISO-C рекомендует использовать для функции rand() следующую линейную конгруэнтную последовательность, параметры которой удовлетворяют указанному критерию: Хм = (X; -1103515245 + 12345) mod m,
260 Криптография на Си и C++ в действии где т = 2*, где к выбирается так, чтобы 2к - 1 было наибольшим числом, которое можно задать типом unsigned int. Значением функции randQ является не Xi+U а Хм/216 mod (RAND_MAX +1), то есть все значения функции rand() заключены между 0 и RAND_MAX. Макрос RAND_MAX определен в файле stdio.h и должен быть по крайней мере не меньше, чем 32267 (см. [Plal], стр. 337). Здесь, очевидно, учтены рекомендации Д. Кнута обходиться без младших двоичных разрядов, когда модуль равен степени двойки. Легко проверить, что условия (а)-(в) выполнены, а значит, указанная последовательность имеет максимально возможный период длины 2*. Удовлетворяет ли указанным условиям конкретная реализация на языке С, исходный текст которой, как правило, неизвестен,1 можно при благоприятных условиях с помощью следующего алгоритма Р. П. Брента. Этот алгоритм вычисляет длину X периода последовательности, вычисленной по рекурсивной формуле Xi+l = F(X,) с помощью порождающей функции F : D —> D из начального значения Х0е D. Для этого требуется не более 2 • тах{|ыД} раз вычислить функцию F (см. [HKW], п. 4.2). Алгоритм Брента определения длины X периода последовательности вида Хм = F(Xi) с начальным элементом Xq 1. Положить у <— Х0, г <— 1 и к <г- 0. 2. Положитьx<r-y,j<r-knr*-r+r. 3. Положить к<г- к+ 1 и у <— F(y). Повторять этот шаг, пока не получится х = у или к > г. 4. Если х Ф у, то вернуться на шаг 2. Иначе результат: Х = к -j. Процесс завершится успехом, если на шаге 3 рассматривать реальные значения F(y\ и неудачей, как для ISO-рекомендации, если вместо значений будут лишь их старшие разряды. Этот тест можно дополнить критерием хи-квадрат (пишут также «критерий х2»), который устанавливает, насколько эмпирически полученное распределение вероятностей соответствует теоретически ожидаемому распределению. При использовании критерия хи-квадрат вычисляют статистику Библиотеки GNU-C от Free Software Foundations и ЕМХ-С Эберхарда Маттеса (Eberhard Mattes) являются приятным исключением. В функции rand() библиотеки ЕМХ используются параметры а = 69069, b = 5 и т = 232. Число а = 69069, предложенное Дж. Марсальей (G. Marsaglia), показывает в сочетании с модулем т = 232 хорошие статистические результаты и максимальную длину периода (см. [Knut], стр. 102-104).
ГЛАВА 11. Большие случайные числа 261 (11-2) 2 = ^(Я(Х,.)-;гР(Х,.))2 х к пР(х{) где для / различных событий X,- через Я(Х,) обозначена наблюдаемая частота события Хь через Р(Х,) - вероятность появления события Х„ а /г - это объем выборки. Для распределения, соответствующего указанному, математическое ожидание статистики х2> рассматриваемой как случайная величина, равно E(%2) = f-l. Пороговые значения, при которых мы отвергаем гипотезу о равенстве распределений с заданной вероятностью ошибки, можно определить по таблицам хи-квадрат распределения с t - 1 степенями свободы (см. [Bosl], п. 4.1). Проверка критерия хи-квадрат применяется для проверки соответствия результатов многих эмпирических тестов теоретически вычисленным распределениям. Особенно легко применять критерий хи-квадрат к последовательностям равномерно распределенных (это и есть гипотеза теста!) случайных чисел X/ из диапазона значений W= {О, ..., w- 1}. Мы считаем, что каждое из чисел множества W может быть взято с одной и той же вероятностью р = 1/w, и таким образом предполагаем, что среди п случайных чисел X, каждое число из W встречается примерно n/w раз (мы считаем п > w). Однако это не обязательно так, поскольку вероятность Рк того, что среди п случайных чисел X, заданное значение w e W появится в точности к раз, вычисляется как Ш) рк=с:Рк(\-ру-к= п- рк(\-Ру-к} .!ti: i, *:)*•'. к\(п-к)\ Это биномиальное распределение действительно принимает наибольшее значение при к ~ n/w, но вероятности Р0 = (1 -р)п и Рп = рп не равны нулю. Следовательно, в предположении, что последо- ! вательность X/ ведет себя как случайная, мы можем ожидать, что * u частоты hw отдельных значений w e W будут распределены по биномиальному закону. Так ли это на самом деле, проверяется по критерию хи-квадрат: (11-4) 2 xWh-nw)2 w^|2 Проверка повторяется для нескольких случайных выборок (отрезков последовательности X/). Грубая аппроксимация %2-распределения позволяет нам сделать вывод, что в большинстве случаев результат %" " С/ - биномиальный коэффициент. - Прим. ред.
262 Криптография на Си и C++ в действии должен лежать в интервале [w-2vw, w + 2vw]. В противном случае данную последовательность можно считать недостаточно случайной. Отсюда следует, что вероятность ошибки, то есть вероятность признать действительно «хорошую» последовательность «плохой» на основании результатов теста хи-квадрат, равна примерно 2%. Подчеркнем, что этот тест корректен только для достаточно большого числа выборок: это число должно быть по крайней мере равно п = 5vv (см. [Bos2], п. 6.1), а в идеале - как можно больше. Линейный конгруэнтный генератор из стандарта ISO-C, который мы рассматривали выше, проходит этот простой тест, как и другие генераторы псевдослучайных чисел, которые нам еще предстоит реализовать в пакете FLINT/C. После такого краткого экскурса в статистику вспомним о том, что случайные последовательности должны удовлетворять, помимо статистических, и другим критериям, в зависимости от области применения. Случайные последовательности, используемые для криптографических приложений, должны быть такими, чтобы без знания некоторой дополнительной (секретной) информации их невозможно было предсказать или восстановить по нескольким заданным элементам. То есть нарушитель не должен иметь возможности воспроизвести криптографический ключ или последовательность ключей, вырабатываемых с помощью псевдослучайной последовательности. Хорошо зарекомендовал себя в этом смысле генератор BBS Л. Блюм (L. Blum), М. Блюма (М. Blum) и М. Шуба (М. Shub), основанный на результатах теории сложности. Сначала опишем, а затем реализуем этот генератор, при этом не будем углубляться в теорию (читатель может с ней ознакомиться в [Blum] или [HKW], глава 4 и п. 6.5). Нам понадобятся два простых числа р и q такие, что p = q = 3 mod 4, их произведение - число п, а также число X взаимно простое с п. Вычислив Хо := X2 mod n, получаем начальный элемент Xq последовательности чисел, получаемых рекуррентным возведением в квадрат: Х/+1 = X? mod n . В качестве случайного числа берем младший бит элемента X;. Полученная таким образом случайная последовательность битов является безопасной с точки зрения криптографии: предсказать следующий бит из уже вычисленных можно только зная делители/? и q числа п. Если же эти два числа хранятся в секрете, то для пред* сказания последующих битов с вероятностью, большей \, или Д^я восстановления неизвестных отрезков последовательности нужно разложить на множители число п. В основе безопасности генератора BBS лежат те же принципы, что и в криптосистеме RSA. За доверь к генератору BBS приходится заплатить трудностью вычисления
ГЛАВА 11. Большие случайные числа 263 6ф случайных битов: для каждого бита нужно возводить в квадрат по модулю большого целого числа, чем и обусловлено длительное время генерации больших случайных последовательностей. Если нужны короткие последовательности случайных чисел, например при генерации отдельных криптографических ключей, это обстоятельство не столь важно. Здесь играет роль лишь вопрос безопасности, хотя при ее оценке следует учитывать и процесс получения начальных значений. Генератор BBS детерминированный, поэтому «чистая случайность» может быть достигнута только при тщательном выборе начального значения. Для этого можно использовать дату или время, статистические характеристики системы (например, число «тиков» системных часов при выполнении заданного процесса), числовые характеристики внешний событий, таких как время между нажатием клавиши клавиатуры или мыши, и многие другие методы, которые лучше всего сочетать друг с другом (советы по выработке начальных значений читатель найдет в работах [East] и [Matt]).3 Теперь вернемся к теме, которой и посвящена эта глава, и на имеющейся базе построим два генератора случайных чисел формата CLINT. В качестве отправной точки на пути к генерации простых чисел научимся, например, создавать большие числа заданной двоичной длины. Для этого старший бит полагаем равным 1, а остальные биты генерируем случайным образом. Сперва построим линейный конгруэнтный генератор и из элементов полученной последовательности будем выбирать разряды случайного числа типа CLINT. Параметры а = 6364136223846793005 и т = 264 для нашего генератора возьмем из таблицы результатов спектрального теста (см. [Knut], стр. 102-104). Тогда последовательность Xi+\ = (X/ • а + 1) mod m будет иметь максимальную длину периода X = т и обладать хорошими статистическими свойствами, что можно заключить на основании результатов, представленных в таблице. Реализуем генератор в виде функции rand64_l. При каждом вызове функции rand64_l очередной элемент последовательности генерируется и записывается в глобальный CLINT-объект SEED64, объявленный как static. Параметр а хранится в глобальной переменной А64. Функция возвращает указатель на SEED64. Функция: Линейный конгруэнтный генератор с периодом 264 Синтаксис: dint * rand64_l (void); [Возврат: Указатель на SEED64 с полученным случайным числом а критических приложениях для генерации начальных значении или всей случайной последовательности всегда следует использовать истинно случайные числа, полученные с помощью подходящих аппаратных компонентов.
264 Криптография на Си и C++ в действии dint * rand64_l (void) mulj (SEED64, A64, SEED64); incj (SEED64); Лля приведения по модулю 2 просто устанавливаем нужную длину поля в SEED64, что не требует почти никаких временных затрат. SETDIGITS_L (SEED64, MIN (DIGITS_L (SEED64), 4)); return ((dint *)SEED64); } Теперь нам нужна функция, задающая начальные значения для rand64_l(). Назовем ее seed64_l(). На вход этой функции поступает переменная типа CLINT, из не более чем четырех старших разрядов которой берется начальное значение переменной SEED64. Предыдущее значение переменной SEED64 копируется в статическую переменную BUFF64 типа CLINT, а возвращает эта функция указатель на BUFF64. | Функция: Задание начальных значений для функции rand64_l() | Синтаксис: dint * seed64J (CLINT seedj); | Вход: seedj (начальное значение) | Возврат: Указатель на переменную BUFF64, содержащую предыдущее значение переменной SEED64 dint * seed64_l (CLINT seedj) { int i; cpyj (BUFF64, SEED64); for (i = 0; i <= MIN (DIGITS J. (seedj), 4); i++) SEED64[i] = seedjfi]; } I
ГЛАВА 11. Большие случайные числа 265 return BUFF64; Еще один вариант функции seed64J() с аргументом типа ULONG Функция: Задание начальных значений для функции rand64J() Синтаксис: dint * ulseed64_l (ULONG seed); Вход: seed (начальное значение) Возврат: Указатель на переменную BUFF64, содержащую предыдущее значение переменной SEED64 dint * ulseed64_l (ULONG seed) cpyj (BUFF64, SEED64); ul2clint_l (SEED64, seed); return BUFF64; Следующая функция возвращает случайные числа типа ULONG. В процессе генерации каждого числа происходит обращение к функции rand64_l(), при этом для построения числа нужного типа используются старшие разряды переменной SEED64. Функция: генерация случайного числа типа unsigned long Синтаксис: unsigned long ulrand64_l (void); Возврат: Случайное число типа unsigned long ULONG ulrand64_l (void) { ULONG val; USHORT I; rand64_l(); 1 = DIGITS_L (SEED64); switch (I) {
266 Криптография на Си и C++ в действии case 4: case 3: case 2: val = (ULONG)SEED64[l-1]; val += ((ULONG)SEED64[l] « BITPERDGT); break; case 1: val = (ULONG)SEED64[l]; break; default: val = 0; } return val; } В пакете FLINT/C есть дополнительные функции ucrand64_l(void) и usrand64_l(void) для генерации случайных чисел типа UCHAR и USHORT соответственно. Здесь мы их обсуждать не будем, а рассмотрим лучше функцию ranf_J(), вырабатывающую случайные числа типа CLINT с заданным числом двоичных разрядов. Генерация случайного числа типа CLINT void randj (CLINT rj, int I); I (число двоичных разрядов - длина генерируемого числа) U (случайное число из интервала 21"1 < rj < 21 - 1) void randj (CLINT rj, int I) { USHORT i, j, Is, Ir; Сначала ограничиваем число двоичных разрядов I максимально допустимым значением для типа CLINT. Затем определяем требуемое число разрядов типа USHORT (Is) и позицию (Ir) старшего двоичного разряда в старшем USHORT-разряде. Функция: Синтаксис: Вход: Выход:
ГЛАВА 11. Большие случайные числа 267 |,<"/Ю;; uS<\. X* _,. .„ --..*••«»» «„..«и* *\ L I = MIN (I, CLINTMAXBIT); Is = (USHORT)I » LDBITPERDGT; lr = (USHORT)I & (BITPERDGT - 1UL); Теперь последовательно генерируем разряды числа г J, каждый раз вызывая функцию usrand64 l(). Таким образом, младшие двоичные разряды числа SEED64 при построении CLINT-разрядов не используются. for (i = 1; i <= Is; i++) { rj[i] = usrand64_l (); Г \ П'.-м:..;, Далее идет «ювелирная обработка» значения r_l - задание старшего бита. Если lr > 0, то бит на (1г-1)-й позиции (ls + 1)-ro USHORT-разряда полагаем равными 1, а все более старшие - равными 0. Если же lr = 0, то ставим 1 в самый старший бит USHORT-разряда с номером Is. if (lr > 0) { U[++ls] = usrand64_l (); j = 1U«(lr-1); /*j<-2A(lr-1)7 U[ls] = (rj[ls]|j)&((j«1)-1); } else { r_l[ls] |= BASEDIV2; } SETDIGITSJ- (rj, Is); } И завершим эту главу реализацией генератора BBS. Для этого с помощью функции primeJO найдем простые числа/? и q такие, что p = q==3 mod 4 примерно с одним и тем же числом двоичных разрядов (это нужно для того, чтобы разложить модуль на множители было максимально трудно, на чем, собственно, и основана криптографическая стойкость генератора BBS, см. стр. 363). Из этих чисел
268 Криптография на Си и C++ в действии abwtMtmw*?? f-лт составляем модуль п = pq. Модуль такого вида длиной 2048 бит читатель найдет в пакете FLINT/C, хотя числа р и q там не указаны (их знает только автор). В static-переменные XBBS и MODBBS будем записывать соответственно элементы последовательности X, и модуль п. Из них функция /. randbitQ вычисляет случайный бит следующим образом. hOi Г *Ч '■ Функция: Синтаксис:"" Возврат: Псевдослучайный генератор Блюм-Блюма-Шуба int randbltj (void); Элемент множества {0, 1} ».<*"iwp3«tf. ■, , г. s -..» * itf , static CLINT XBBS, MODBBS; static const char *MODBBSSTR = "81aa5c..."; /* Модуль как строка символов */ int randbitj (void) { msqrj (XBBS, XBBS, MODBBS); ( \ В качестве результата берем младший бит числа XBBS. return (*LSDPTR_L (XBBS) & 1); } Для инициализации генератора BBS воспользуемся функцией seedBBSJQ. Функция: Задание начальных значений для функций randbit_l() и randBBSJO Синтаксис: int seedBBSJ (CLINT seedj); Вход: seedj (начальное значение) int seedBBSJ (CLINT seedj) { CLINT gcdj;
ГЛАВА 11. Большие случайные числа 269 str2clint_l (MODBBS, (char *)MODBBSSTR, 16); gcd_l (seedJ, MODBBS, gcdj); if(!EQONE_L(gcd_l)) { return -1; } msqrj (seedj, XBBS, MODBBS); return 0; } Функция ulrandBBSJO, которую тоже можно использовать для генерации случайных чисел типа ULONG, аналогична функции ulrand64J(). Функция: Генерация случайного числа типа unsigned long Синтаксис: unsigned long ulrandBBSJ (void); Возврат: Случайное число типа unsigned long ULONG ulrandBBSJ (void) { ULONG i,r = 0; for (i = 0; i < (sizeof(ULONG) « 3); i++) r = (r« 1) + randbitJQ; } return r; } Нам не хватает еще функции randBBSJ(CLINT rj, int I), генерирующей случайные числа rj длины ровно I двоичных разрядов, то есть rj из интервала 21"1 < г J < 21 - 1. Мы не приводим здесь ее описание, поскольку она во многом совпадает с функцией randj(). Но разумеется, эту функцию читатель найдет в пакете FLINT/C.
A 12. атегия тестирования LINT Не обвиняйте компилятор. Дэвид А. Спулер, C+ + и С: Отладка, тестирование и достоверность кода В предыдущих главах не раз упоминалось о тестировании отдельных функций. Без проведения осмысленных тестов, удостоверяющих качество нашего пакета, вся проделанная нами работа оказалась бы бесполезной, ибо чем ещё можно обосновать нашу уверенность в надежности разработанных функций? Поэтому сейчас мы собираемся посвятить всё внимание этой важной теме, и с этой целью поставим перед собой два вопроса, которыми должен задаваться каждый разработчик программного обеспечения: • Как удостовериться, что функции нашего программного обеспечения ведут себя в соответствии с их спецификацией, которая в нашем случае означает в первую очередь то, что они математически корректны? • Как достичь стабильности и надёжности функционирования нашего программного обеспечения? Несмотря на то, что эти два вопроса тесно связаны, фактически они относятся к двум различным областям. Функция может быть математически некорректна, например, если был неправильно реализован базовый алгоритм, и всё же она может надёжно и стабильно воспроизводить эту ошибку и постоянно выдавать один и тот же неправильный результат для заданного входного значения. С другой стороны, функции, возвращающие правильные на вид результаты, могут быть подвержены ошибкам другого рода, например таким, как переполнение длины вектора или использование неправильно инициализированных переменных, что приводит к неопределённости поведения. Причём эта неопределённость остаётся невыявленной после удачного (или лучше сказать неудачного?) завершения отладки. Итак, мы должны иметь в виду оба эти аспекта и утвердить методы разработки и тестирования, которые позволят нам доверять как корректности, так и надёжности наших программ. Существуют многочисленные публикации, в которых обсуждается значение и послед- ' ствия таких требований для всего процесса разработки программного обеспечения и где углублённо изучается проблема качества программ. Такое почтительное внимание к этой теме нашло выражение в международной тенденции внедрять в производство программного обеспечения стандарт ISO 9000. Теперь больше не говорят просто о "тестировании" или "обеспечении качества", вместо этого слышны разговоры об "управлении качеством" или о "полном
272 Криптография на Си и C++ в действии управлении качеством". Отчасти это просто результат эффективного маркетинга, но, тем не менее, эти формулировки должным образом освещают проблему, состоящую в том, чтобы рассматривать процесс создания программного обеспечения во всей его многосторонней полноте и посредством этого улучшать его. Часто употребляемое выражение "проектирование программного обеспечения", или >w "программирование" не может скрыть тот факт, что этот процесс, л-*:»<л. если уЧесть его отношение к предсказуемости и точности, едва ли может соперничать с классическим инженерным искусством. **.'»•}■. Это сравнение должным образом характеризуется следующим анекдотом. Три инженера - механик, электрик и программист - решили вместе прокатиться на автомобиле. Они уселись в машину, но та отказалась заводиться. Механик сразу же заявил: «Проблема с мотором. Засорена форсунка инжектора». «Чепуха, - возразил элек- *:;. трик. - Виновата электроника. Определенно отказала система зажигания». После чего программист предложил: «А давайте все вылезем из машины и опять залезем. Может, тогда она заведется». Оставим трех отважных инженеров с их дальнейшими разговорами и . » и приключениями и рассмотрим некоторые опции, которые были реализованы при создании и тестировании пакета FLINT/C. Прежде всего, упомянем те литературные источники, которыми мы пользо- к*'{'' ! вались. Они не утомляют читателя абстрактными рассуждениями и руководящими указаниями, а оказывают конкретную помощь в решении конкретных проблем, не упуская из виду общей картины1. Каждая из этих книг содержит многочисленные ссылки на другую важную литературу по этой теме: • [Dene] - стандартная работа, рассматривающая процесс разработки программного обеспечения во всей полноте. Книга содержит много методологических указаний, основанных на практическом опыте автора, а также много наглядных и полезных примеров. Снова и снова затрагивается тема тестирования в связи с разнообразными этапами программирования и системного интегрирования, при этом основные концептуальные и методологические правила рассматриваются совокупно с практической точки зрения, и всё это объединено тщательно спроектированной системой примеров. • [Harb] - содержит полное описание языка программирования С и стандартной библиотеки С, а также даёт много ценных указаний и замечаний по поводу стандарта ISO. Это необходимое справочное пособие, к которому можно обращаться на каждом шагу. • [Hatt] - очень подробно, в деталях, описывает создание в языке С систем программного обеспечения с критической надёжностью. Указанные здесь источники представляют личную, субъективную выборку автора. Существует много других книг и публикаций, которые также можно было бы включить в этот список, однако за недостатком места и времени они были опущены.
ГЛАВА 12. Стратегия тестирования LINT 273 Типовые примеры и источники ошибок демонстрируются с помощью конкретных примеров и статистики - а ведь язык С определённо предоставляет много возможностей для ошибок. Также приводятся исчерпывающие методологические советы, следуя которым, можно укрепить доверие к продуктам программного обеспечения. • [Lind] - превосходная, занимательно написанная книга, показывающая глубокое понимание автором языка программирования С. Кроме того, автор знает, как передать своё понимание читателю. Многие рассматриваемые темы можно было бы снабдить подзаголовком «А вы знаете, что...», и очень немногие читатели смогли бы честно, положа руку на сердце, ответить утвердительно. • [Magu] - рассматривает проектирование подсистем и поэтому представляет для нас особенный интерес. Здесь идёт речь об интерпретации интерфейсов и принципах работы с функциями, имеющими входные параметры. Разъясняются также отличия между рискованным и защитным программированием. Ещё одна сильная сторона этой книги - эффективное использование утверждений (см. стр. 173) в качестве средств тестирования и во избежание неопределённых программных состояний. • [Murp] - содержит описание множества средств тестирования, которые можно, не прилагая больших усилий, применить на практике при тестировании программ и немедленно получить успешные результаты. Помимо всего прочего к этой книге прилагается дискета с библиотеками для реализации утверждений, тестирования обработки объектов динамической памяти, и отчёта о выполнении тестов. Эти библиотеки также использовались для тестирования FLINT/C-функций. • [Spul] - предлагает для обозрения методы и средства тестирования программ на языках С и C++ и даёт многочисленные указания по их эффективному применению. Книга содержит широкий обзор типичных для С и C++ ошибок программирования и рассматривает способы их обнаружения и устранения. 12.1. Статический анализ Методологические подходы к тестированию можно разделить на две категории: статическое тестирование и динамическое тести- * рование. К первой категории относится проверка кода (текста программы). При этом исходный текст внимательно просматривается и , проверяется построчно на наличие таких проблем, как отклонения %) от спецификации (в нашем случае это выбранные алгоритмы), ошибки в рассуждениях, неточности в расположении строк или в . стиле, сомнительные конструкции и ненужные кодовые последова- ^ тельности. 1
274 Криптография на Си и C++ в действии Для проверки кодов используются аналитические средства, такие как хорошо известная в Unix программа lint, которые в значительной степени автоматизируют эту трудоёмкую задачу. Первоначально одним из главных применений lint было компенсировать существовавшие ранее в языке С недочёты при проверке согласования параметров, которые передавались функциям в транслируемые по отдельности модули. Тем временем появились более удобные, чем классический lint, продукты, которые могли обнаруживать огромное количество потенциальных проблем в коде программы. Причём синтаксические ошибки, не позволяющие компилятору транслировать код, представляли лишь малую часть этих проблем. Ниже приводятся несколько примеров проблемных областей, которые можно обнаружить путём статического анализа: • синтаксические ошибки, • пропущенные или несогласованные прототипы функций, • несогласования при передаче параметров функциям, • ссылки на несовместимые типы или соединение таких типов, • использование неинициализированных переменных, • непереносимые конструкции, • необычное или неправдоподобное использование отдельных языковых конструкций, • недостижимый код. Настоятельным условием строгой проверки типов автоматизированными средствами является использование прототипов функций. С помощью прототипов ISO-совместимый компилятор языка С может проверять во всех модулях типы передаваемых функциям аргументов и определять несогласования. Многие компиляторы тоже можно настроить на анализ исходного кода, если включены соответствующие уровни предупреждений. Например, компилятор языка C/C++ gcc из проекта GNU Free Software Foundation обладает весьма мощными анализирующими функциями, которые можно активировать с помощью опций -Wall -ansi и -pedantic2. При установке FLINT/C-функций, кроме тестов, выполняемых множеством разных компиляторов, для статического тестирования прежде всего применялись продукт PC-lint из Gimpel Software (версия 7.5; см. [Gimp]) и LCLint из MIT (версия 2.4; см [Evan]) . Этот компилятор содержится в различных дистрибутивах Linux, а также его можно приобрести на http://www.leo.org. LCLint можно скачать из Интернета. Домашняя страничка LCLint находится по адресу http://www.sds.lcs.mit.edu/pub/lclint/. По анонимному ftp можно скачать LCLint для Linux Windows 9x/NT по адресу ftp://sds.lcs.mit.edu/pub/lclint/. зсу 1 л
ГЛАВА 12. Стратегия тестирования LINT 275 PC-lint оказался весьма полезным инструментом для тестирования программ как на языке С, так и на C++. Ему известны примерно две тысячи отдельных проблем, и он использует механизмы, которые извлекают из кода значения, загруженные в динамические локальные переменные во время выполнения, и включают их в сообщения об ошибках. Таким путём уже во время статического анализа можно открыть многие ошибки, такие как превышение границ векторов, которые обычно обнаруживаются - если обнаруживаются вообще - только в процессе выполнения (и можно надеяться, что во время тестирования, а не после него). Помимо этого, доступный для широкого пользования LCLint был переделан для работы под операционной системой Linux. LCLint различает четыре режима (weak, standard; check, strict), каждый из которых связан с определёнными предварительными установками. Эти режимы выполняют тесты различной степени точности. Кроме типовых для lint функций, LCLint делает возможным проверку программ на наличие отдельных спецификаций, которые вставляются в исходный текст в виде особым образом форматированных комментариев. Таким способом можно сформулировать граничные условия для реализации функций и их вызовов и проверить их соответствие спецификациям. Имеются также дополнительные возможности для семантического управления. Для программ без дополнительных спецификаций в качестве стандарта рекомендуется установка режима с опцией -weak. Тем не менее, в руководстве упоминается о специальной награде для того, кому впервые удастся написать «настоящую программу», не выдающую ошибок при использовании LCLint в режиме -strict. При использовании этих двух инструментов для тестирования FLINT/C-функций оказалось целесообразным предварительно проверять, какие именно опции используются, и создавать файлы параметров пользователя, чтобы сконфигурировать инструменты для индивидуального применения. После тщательной проверки FLINT/C-кода, по окончании теста ни один из этих двух продуктов не выдал никаких предупреждений, которые можно было бы счесть серьёзными. Это позволяет нам надеяться на успешное выполнение поставленных выше условий, гарантирующих качество FLINT/C-функций. 12.2. Динамические тесты Цель динамических тестов в том, чтобы доказать, что отдельный «кирпичик» какой-либо части программного обеспечения выполняет свою спецификацию. Чтобы придать этим тестам выразительность и силу для оправдания затраченных на них времени и денег, нам придётся предъявить к ним те же требования, что и к научному эксперименту. А именно: они должны быть полностью документированы,
276 Криптография на Си и C++ в действии а их результаты должны быть воспроизводимы и доступны для проверки сторонними наблюдателями. Полезно делать различие между тестированием отдельных модулей и тестами интегрированных систем, хотя границы здесь весьма подвижны (см. [Dene], п. 16.1). Для достижения этой цели тесты, или контрольные примеры, следует конструировать таким образом, чтобы можно было проверять функции предельно исчерпывающим образом или, иными словами, достигать максимально возможного охвата тестируемых функций. Критерии, или метрики, для охвата тестов могут быть различными. Например, в случае СО-охвата оценивается доля инструкций функции или модуля, которые на самом деле выполняются или, если конкретно, какие инструкции не выполняются. Есть критерии большего размера, чем СО, которые учитывают долю использующихся ветвей программы {С 1-охват) или даже долю пройденных путей функции. Последний значительно сложнее первых двух. В каждом случае целью является достижение как можно большего охвата с помощью тестов, которые полностью контролируют поведение программы. Здесь есть два аспекта, слабо связанных друг с другом. Тестировщик, просматривающий все ветви функции, всё же может не обнаружить некоторые ошибки. С другой стороны, можно сконструировать контрольные примеры, в которых тестируются все свойства функции, даже если некоторые её ветви не рассматриваются. Таким образом, качество теста можно оценивать по крайней мере двумя измерениями. Тестовые значения, основанные только на знании спецификации, выполняют так называемые тесты на уровне черного ящика. Если для достижения высокой степени охвата тестов этого недостаточно, тогда необходимо при конструировании контрольных примеров учитывать подробности реализации, и в этом случае мы получим так называемые тесты на уровне белого ящика. Примером случая, когда мы создавали тесты для особой ветви функции только на основе спецификации, является алгоритм деления на стр. 67: чтобы протестировать шаг 5, используются специальные тестовые данные, указанные на стр. 79, в результате чего выполняется соответствующий код. С другой стороны, необходимость специальных тестовых данных при делении с меньшими делителями становится очевидной, только если учитывать, что этот процесс выполняется особой частью функции div__l(). Здесь подразумевается именно подробность реализации, которую нельзя вывести из алгоритма. На практике обычно всё сводится к смешанному применению методов чёрного и белого ящиков, которое в работе [Dene] называется соответственно тестирование серого ящика. Однако, никогда нельзя ожидать достижения стопроцентного охвата, как показываю следующие рассуждения: допустим, что мы генерируем просты6 числа с проверкой по тесту Миллера-Рабина с большим число*1 итераций (скажем, 50) и соответствующей вероятностью ошибке
iABA12. Стратегия тестирования LINT 277 ((1/4)_5U ~ 10~~зи, см. п. 10.5) и затем проверяем эти простые числа ещё одним, детерминированным, тестом на простоту. Поскольку управление передаётся той или иной ветви программы, в зависимости от исхода этой второй проверки, у нас практически нет реального шанса достичь той ветви, переход к которой следует только после отрицательного результата проверки. Однако вероятность того, что эта вызывающая подозрение ветвь будет выполняться при . s действительном использовании программы, также нереальна, поэтому, наверное, легче обойтись без этой части теста, чем вносить в код семантические изменения ради искусственного создания возможности тестирования. Таким образом, на практике всегда -'у можно встретить ситуации, где требуется отказаться от стопроцентного тестового охвата, чем бы он ни измерялся. Тестирование арифметических функций пакета FLINT/C, который :-i. реализован главным образом с математической точки зрения - весьма сложная задача. Как установить, выдают ли правильные результаты операции сложения, умножения, деления или возведения в степень больших чисел? Карманные калькуляторы, как правило, вычисляют только порядок величины, эквивалентной вычисляемой ' стандартными арифметическими функциями С-компилятора, поэтому значимость этих вычислений ограниченна. Конечно, имеется вариант, при котором можно применить в качестве теста другой арифметический программный пакет. Разработаем необходимый интерфейс, преобразуем числовые форматы - и пусть ' '' функции «соревнуются» друг с другом. Однако против этого подхода есть два «но»: во-первых, это не развлечение, а во-вторых, почему нужно доверять чьим-то разработкам, о которых известно значительно меньше, чем о собственном продукте? Поэтому поищем другие возможности тестирования, и с этой целью применим v> математические структуры и законы, достаточно мощные, чтобы г;. распознать вычислительные ошибки в программе. С обнаружении ными ошибками затем можно будет разобраться с помощью допол- 1, j нительного вывода тестовых результатов и современного символь- uiv ного отладчика. Поэтому мы избирательно применим метод черного ящика и будем надеяться, что к концу этой главы мы составим практичный план '* проведения динамических тестов, который будет придерживаться того курса проведения тестирования, который применялся к "^ * FLINT/C-функциям. В ходе этого процесса у нас была цель достичь наибольшего С1-охвата, хотя никакие измерения в этом отношении не производились. Перечень свойств FLINT/C-функций, которые нужно протестировать, не особенно велик, но затрагивает существенные вопросы. В частности, мы должны убедиться в следующем. • Все результаты вычислений корректны над всей областью определения всех функций.
278 Криптография на Си и C++ в действии i/ Все входные значения, для которых внутри функции имеются специальные последовательности команд, обрабатываются правильно. • Осуществляется правильное управление переполнением и потерей значимости. То есть все арифметические операции выполняются по модулю Nmax + 1. ^ Ведущие нули допускаются, не влияя на результат. • Вызовы функции в режиме сумматора с идентичными объектами памяти в качестве переменных, например, такие как add_l(n_J, nj, nj), возвращают правильные результаты. ^ Все деления на нуль распознаются, и выдаётся соответствующее сообщение об ошибке. Имеется много отдельных тестовых функций, необходимых для работы с этим списком, функций, которые вызывают тестируемые FLINT/C-операции и проверяют их результаты. Эти функции собраны в тестовых модулях, и их самих проверяют каждую по отдельности, прежде чем применить к FLINT/C-функциям. Для проверки тестовых функций используются те же самые критерии и те же самые средства статического анализа, что и для FLINT/C- функций. Более того, выполнение тестовых функции следует просмотреть пошагово по крайней мере на имеющейся в наличии контрольной базе с помощью символьного отладчика для того, чтобы проверить, тестируют ли они то, что нужно. Чтобы определить, действительно ли тестовые функции должным образом реагируют на ошибки, полезно встроить в арифметические функции ошибки, приводящие к неправильным результатам (а после этапа тестирования удалить их, не оставив и следа!). Поскольку мы не можем протестировать каждое значение из области определения CLINT-объектов, нам требуются, кроме фиксированных заданных заранее тестовых значений, случайным образом сгенерированные входные значения, которые равномерно распределены по всей области определения [0, Nm.dX]. С этой целью используем нашу функцию rand_l(r_l, bitlen), при этом выбираем число двоичных разрядов bitlen, с помощью приведения функции usrand64_l() по модулю (МАХ2 + 1) случайным образом из интервала [О, МАХ2]. Первыми должны проходить тестирование функции для генерации псевдослучайных чисел, которые были рассмотрены в главе 11, где среди прочего мы применяли описанный там критерий %2 для проверки статистических свойств функций usrand64_l() и usrandBBSJO- Kp°* ме этого, мы должны убедиться, что функции rand_l() и randBBSjO правильно генерируют числовой формат CLINT и возвращают числа точно той длины, которая предопределена. Эта проверка необходима и для других функций, выдающих CLINT-значения. Для распознавания ошибочных форматов CLINT-аргументов у нас есть функЦИ* vcheck_l(), которую поэтому следует поместить в начало последовательности тестов.
ГЛАВА 12. Стратегия тестирования LINT 279 Ещё одно условие для большинства тестов - это возможность определения равенства или неравенства и сравнения размеров целых чисел, представленных CLINT-объектами. Мы должны также протестировать функции ldj(), equ_l(), mequ_l() и cmp_l(). Это можно осуществить, используя как определённые заранее, так и случайные числа, при этом следует проверять все случаи - равенство, так же как и неравенство - с соответствующими соотношениями размеров. Ввод заранее заданных значений производится, в зависимости от цели, либо посредством функции str2clint_l(), либо в виде типа ■ '•*• unsigned посредством функции преобразования u2clint_l() или ul2clint_l(). Функция str2clint_l(), обратная к функции xclint2str_l(), • 1 "","', используется для генерации выходного результата теста. Поэтому эти функции должны стоять следующими в нашем списке тестируемых функций. При тестировании строковых функций мы воспользуемся их взаимодополняемостью и проверим, получается ли в результате выполнения одной функции после другой исходная строка символов или, при выполнении в другом порядке, выходное значение в CLINT-формате. Далее мы неоднократно будем возвращаться к этому правилу. Теперь остается проверить только динамические регистры и их ;'С'г?'.'...> управляющие механизмы, описанные в главе 9, которые вообще хотелось бы включить в тестовые функции. Использование регистров как динамически распределённой памяти помогает нам тестировать FLINT/C-функции, при этом мы дополнительно реализуем отладоч- ,()|1 ную библиотеку для функций malloc() для распределения памяти. Типовая функция таких пакетов, как общедоступных, так и коммерческих (см. [Spul], глава 11), проверяет поддержание границ динамически распределённой памяти. Имея доступ к CLINT-регистрам, мы можем следить за нашими FLINT/C-функциями: о каждом вторжении границы в чужую область памяти будет сообщаться. Типовой механизм, предоставляющий такую возможность, перена- ^ r>;i правляет обращенные к malloc() вызовы специальной тестовой f f ♦,, *, функции, которая получает запросы на выделение памяти, по оче- I п*,. ш реди вызывает malloc() и таким образом выделяет значительно i K v .|Ч больше памяти, чем требуется на самом деле. Этот блок памяти i и • , регистрируется во внутренней структуре данных, а справа и слева I". от первоначально запрашиваемой области памяти создаётся «барьер» из нескольких бит, которые заполняется избыточным шаблоном, ***чкл»»*<л. таким как чередующиеся двоичные нули и единицы. Затем возвра- .-ч/нмУ щается указатель на свободную память внутри этого барьера. ' ' ** Теперь обращение к функции free() также направляется сначала к отладчику этой функции. Прежде чем освобождается выделенный блок, выполняется проверка того, остался ли «барьер» неповреждённым или шаблон уничтожен путём затирания, и в этом случае генерируется соответствующее сообщение и область памяти вычёркивается из регистрационного списка. Только потом в действительности вызывается функция free(). В заключение можно, используя
280 Криптография на Си и C++ в действии внутренний регистрационный список, проверить, какие области памяти не были освобождены. Настройка кода на передачу вызовов, обращенных к malloc() и free(), их отладочным вариантам осу. ществляется с помощью макросов, которые, как правило, описаны в файлах #include. Для тестирования FLINT/C-функций применяется пакет ResTrack, описанный в [Murp]. Его использование даёт возможность обнаружить, при определённых обстоятельствах, трудно уловимые случаи превышения векторных границ CLINT-объектов, которые иначе могли бы остаться необнаруженными во время тестирования. Теперь, когда мы закончили основные приготовления, рассмотрим функции, выполняющие основные вычисления (см. главу 4). addj(), subJO, mulJO, sqr_l(), divj(), modJO, incJQ, dec_l(), shlj(), shr_l(), shiftJO, включая базовые функции add(), sub(), multQ, umul(), sqr(), смешанные арифметические функции с аргументами типа USHORT uaddJO, usub_l(), umulJO, udivJQ, umod_l(), mod2_l(), и, наконец, функции модульной арифметики (см. главы 5 и 6) madd_l(), msub_l(), mmulj(), msqr_l(), и функцию возведения в степень *mexp*_l(). Правила вычислений, которыми мы будем пользоваться при тестировании этих функций, вытекают из групповых законов для целых чисел, которые уже приводились в главе 5 для кольца классов вычетов Zn. Здесь мы снова приводим подходящие правила для натуральных чисел и можем придумать тест в любом случае, если между выражениями стоит знак равенства (см. таблицу 12.1). Таблица 12.1. Групповой закон аля целых чисел, используемый при тестировании Тождествен ность Коммутативный закон Ассоциативный закон Сложение а + 0 = а а + b = b + а (а +Ь) + с = а + (Ь + с) Умножение а • 1 =а а .Ь = Ь-а (а .Ь).с = аАЬ
ГЛАВА 12. Стратегия тестирования LINT 281 Сложение и умножение можно проверять относительно друг друга, используя определение к ка:=^а, i=i по крайней мере, для малых значений к. Следующие соотношения, которые поддаются тестированию, - это дистрибутивный закон и первая формула бинома Ньютона. Закон дистрибутивности: а • (Ь + с) = а - Ъ + а • с Формула бинома Ньютона: (а + Ь)2 = а2 + 2ab + Ь2 Законы сокращенного сложения и умножения предоставляют следующие возможности для проверки сложения и вычитания, так же как умножения и деления: а + Ь = с=>с-а = Ьис-Ь = а и а • Ъ = с => с/а = Ъ и clb = a. Деление с остатком можно проверить умножением и сложением, используя функцию деления, чтобы вычислить для делимого а и делителя Ь, сначала частное q и остаток г. Затем в игру вводятся умножение и сложение, чтобы проверить, выполняется ли равенство а = Ъ- q + г. Для проверки модульного возведения в степень с помощью умножения для малых к мы прибегаем к помощи определения: к ак := I I а . /=1 Отсюда можно перейти к правилам возведения в степень (см. главу 1) ars = (aj ar+s = ar. a\ которые являются основой для проверки возведения в степень умножением и сложением. Кроме этих и других тестов, основанных на правилах арифметических вычислений, мы используем специальные тестовые подпрограммы, которые проверяют оставшиеся функции из вышеприведённого списка, и, в частности, поведение этих функций на границах областей определения СLINT-объектов или в других ситуациях, критических для отдельных функций. Некоторые из этих тестов находятся в тестовом комплекте пакета FLINT/C, входящем в
282 Криптография на Си и C++ в действии сопроводительный CD-ROM. Этот тестовый пакет содержит модули, перечисленные в таблице 12.2. Таблииа 12.2. Тестовые функиии пакета FLINT/C Имя модуля testrand.c testbbs.c testreg.c testbas.c testadd.c testsub.c testmul.c testkar.c testsqr.c testdiv.c testmadd.c testmsub.c testmmul.c testmsqr.c testmexp.c testset.c testshft.c testbool.c testiroo.c testggt.c Содержание теста Линейные сравнения, генератор псевдослучайных чисел Генератор псевдослучайных чисел Блюм-Блюма-Шуба Управление регистрами Базовые функиии cpy_l(), ld_l(), equ_l(), mequ_l(), cmp_l(), u2clint_l(), ul2clint_l()/ str2clint_l(), • xclint2str_l() Сложение, включая inc_l() Вычитание, включая dec_l() Умножение Умножение методом Караиубы Возведение в квадрат Деление с остатком Модульное сложение Модульное вычитание Модульное умножение Модульное возведение в квадрат Модульное возведение в степень Функиии доступа к битам Операиии сдвига Булевы операиии Извлечение иелого квадратного корня да Вычисление наибольшего обшего делителя и наименьшего обшего кратного Мы вернемся к нашим теоретико-числовым функциям в конце второй части этой книги, где они приведены в качестве упражнении для заинтересованного читателя (см. главу 17).
Часть II Класс LINT: арифметика на C++ Анатомические находки, составляющие предмет исследования науки, широко распространены в качестве украшений при создании объектов в различных географических зонах и антропо-этнических группах. Добытый человеком фрагмент, обычно кость, становится функциональной деталью конструкции объектов. Кость, как минимум, частично утрачивает свою анатомическую индивидуальность, в том смысле, что ее обрабатывают, обращаясь с ней так, что она становится неотъемлемой частью объекта, тем самым приобретая символическое значение, выходящее далеко за пределы телесной сущности. Надпись на этикетке экспоната национального музея Антропологии и Этнологии, Флоренция, Италия.
ГЛАВА 13. Пусть C++ облегчит Вашу жизнь Детали разменяли нашу жизнь на мелочи... Упрощайте, упрощайте. А. Д. Торо, Вальден. Язык программирования C++, разрабатывавшийся с 1979 года Бьярном Страуструпом1 в Bell Laboratories, является расширением языка С, и его роль становится преобладающей по отношению к другим языкам в области создания программных продуктов. Язык C++ поддерживает принципы объектно-ориентированного программирования, основой которого являются программы, а точнее сказать, процессы, включающие в себя множества объектов, которые взаимодействуют исключительно через их интерфейсы. То есть они обмениваются информацией или принимают определенные внешние команды и обрабатывают их как задачу. В этом интерфейсе методы, с помощью которых выполняется задача, являются подзадачей, "определенной на" автономии единственного объекта. Структуры данных и функции, которые представляют внутреннее состояние объекта и эффект переходов между состояниями, явля- "' ются частным делом объекта и не должны быть обнаружены со стороны. Данный принцип, известный как намеренное скрытие и информации от пользователя, помогает разработчикам программного обеспечения сосредоточиться над задачами, в которых объект выполняется внутри структуры программы, что позволяет не вдаваться в детали реализации. (Говоря другими словами, мы заостряем внимание на том, "что", а не "как"). ),. Структурные единицы, которые имеют дело с «частными делами» т объекта и содержат полную информацию об организации структур ;:* данных и функций, называются классами. Наряду с этим создаются >?. внешние интерфейсы объекта, которые и определяют набор пове- vi дений, который объект может выполнять. Так как все объекты ^jl; ; класса имеют одинаковый самый структурный дизайн, они тоже Интернет-страница Бьярна Страуструпа (hhtp://www.research.att.com/4bs/), возможно, поможет ответить на вопрос «Как Вы произносите "Бьярн Страуструп"?»: "Это может быть затруднительным для людей, которые не являются скандинавами. Лучший совет, который я до сих пор слышал, это 'начать выговаривать имя и фамилию несколько раз на норвежском языке, затем Забить горло картошкой и повторить это снова':-). Оба моих имени произносятся по слогам: BJar-ne Strou-strup. Как В, так и J в моем имени не являются ударными, a NE - достаточно с^абые, поэтому Be-ar-neh или Ву-ar-ne наталкивает на правильный путь. Первое U в моей фами- Лии на самом деле должно было быть V, первый конечный слог произносится глубоким гортанным голосом: Strov-strup. Второе U слегка похоже на 00 в OOP, однако оно остается коротким; возможно Strov-stroop подаст какую-нибудь идею. " (Как видно, устоявшаяся русская транскрипция также имеет мало общего с оригиналом, имя должно звучать как что-то похожее на ^ьярне Стравструп» - Прим. перев.)
286 Криптография на Си и C++ в действии обладают одним и тем же интерфейсом. Но как только объекты были созданы (компьютерные разработчики говорят, что класс тиражирует (instantiate) объекты), они существуют независимо. Их внутреннее состояние меняется независимо друг от друга, кроме того, они запускают различные задачи в соответствии с их ролями в программе. Объектно-ориентированное программирование распространяет использование классов в качестве стандартных блоков больших структур, которые также могут являться классами или группами классов, в готовые программы, так же, как и дома или автомобили делаются из сборных модулей. В идеальном варианте, программы могут быть собраны вместе из библиотек заранее созданных классов без необходимости создания значимой части нового кода (во всяком случае, не в такой степени, как для традиционной разработки программ). В результате этого проще разрабатывать программу применительно к текущей ситуации, прямо моделировать текущие процессы, таким образом проходя последовательные уточнения, пока результатом не будет набор объектов отдельных классов и их взаимосвязей, причем все еще можно распознать модель реальности, которая лежит в их основе. Такой порядок выполнения действий достаточно хорошо известен нам из многих жизненных аспектов, то есть мы не работаем напрямую с необработанными материалами, если мы хотим что-либо создать скорее мы станем применять готовые модули, о конструкции которых или внутренней разработке в деталях мы ничего не знаем. Просто в этих знаниях нет необходимости. Опираясь на имеющийся опыт, мы получаем возможность создания все более и более сложных структур. В процессе создания программ предыдущий опыт разработок раньше не находил применения, разработчики программных продуктов постоянно возвращались к тому, что создавали ранее. Программы создавались с помощью элементарных операций языка программирования (такой конструктивный процесс обычно называют кодированием). Применение библиотек этапа исполнения, таких как стандартная библиотека С, не улучшает существенно эту ситуацию, так как функции, содержащиеся в подобных библиотеках, являются слишком примитивными, чтобы обеспечить связь с более сложными приложениями. Любой программист знает, что структуры данных и функции, которые являются подходящим решением для некоторых проблем, лишь изредка можно применить в похожих, но тем не менее разнЫ задачах без модификации. И как результат - практическое отсу ствие выгоды от оттестированных и доверенных компонентов, любое их изменение содержит риск появления новых ошибок - к в проектировании, так и в программировании. (Это напомина предупреждение в инструкции по эксплуатации любого товар ^ "Любое изменение, вносимое лицом, не являющимся авторизовз ным специалистом, отменяет гарантию").
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 287 С целью повторного использования программ в форме готовых компонент, среди огромного количества других концепций был разработан принцип наследования. Это дает возможность модифицировать классы, для того чтобы удовлетворять новым требованиям, фактически не изменяя их. Вместо этого необходимые изменения будут внесены на уровне расширений. Объекты, которые появились таким образом, помимо новых свойств, приобретают все свойства старых объектов. Можно сказать, что они наследуют эти свойства. Принцип скрытия информации остается незыблемым. Вероятность ошибки значительно уменьшилась, а производительность возросла. Все это выглядит так, словно сбываются мечты. Как объектно-ориентированный язык программирования, C++ обладает всеми необходимыми механизмами для поддержки этих принципов абстракции2. Тем не менее, эти механизмы представляют собой лишь возможность, а не гарантию того, что они будут использоваться так, как принято в объектно-ориентированном программировании. С другой стороны, переход от традиционной к объектно-ориентированной разработке программного обеспечения [' требует значительной интеллектуальной перестройки. Особенно [ явно это отражается в двух отношениях: с одной стороны, разработчик, который к настоящему времени достиг больших успехов, вынужденно посвяшает значительно больше внимания фазам моде- г о лирования и проектирования, нежели тому, что обычно требовалось в традиционных методах разработки программ. С другой стороны, /\ ■ при разработке и тестировании новых классов особое внимание нужно обращать на то, чтобы компоновочные блоки были безоши- > бочны, так как они будут использоваться в множестве программ, гк которые будут разрабатываться в дальнейшем. Также скрытие чип информации может означать скрытие ошибок, Исказится цель идеи KIJ * объектно-ориентированного программирования, если пользователю Id,! у класса придется знакомиться с внутренней его организацией для шг ,. того, чтобы найти ошибку. Результатом этого являются ошибки, р' содержащиеся в реализации класса, которые наследуются вместе с к :, классом, и все подклассы становятся зараженными "наследствен- Ь1^ ным заболеванием". С другой стороны, анализ ошибок в объектах класса может быть ограничен реализацией самого класса, что может значительно уменьшить границы поиска ошибки. В конечном счете нам следует отметить, что, хотя существует устойчивая тенденция в применении языка C++ в качестве языка программирования, принципы объектно-ориентированного программирования, воплощенные в чрезвычайно сложных элементах языка C++ - не единственный объектно-ориентированный язык программирования. Например, есть такие языки как Simula (предшественник всех объектно-ориентированных языков), Smalltalk, Eiffel, Oberon, и Java.
288 Криптография на Си и C++ в действии C++, часто многогранны и лежат за пределами понимания. Поэтому пройдет много времени, прежде чем этот метод станет стандартным для разработки программного обеспечения. Таким образом, название данной главы отражает не столько объектно-ориентированное программирование и применение языка C++ в целом, сколько механизмы, которые в ней предложены, и их значение в нашем проекте. Они дают возможность описать арифметически операции с большими числами настолько же естественно, как это было бы со стандартными операциями языка программирования. Поэтому в последующих разделах мы рассмотрим не введение в язык C++, а рассуждения о разработке классов, представляющих длинные натуральные числа и экспортируемые функции для работы с ними в виде абстрактных методов3. Некоторые подробности о структурах данных будут скрыты как от пользователя, так и от клиента класса, так же как и реализации различных арифметических и теоретико-числовых функций. Тем не менее, прежде чем воспользоваться классами, они должны быть уже созданы, поэтому нам следует иметь представление об их содержимом. Однако ни для кого не должно быть неожиданностью то, что мы не начнем с самого начала, а воспользуемся работой, которую мы уже завершили в первой части книги, и определим наш арифметический класс как абстрактный уровень, или же оболочку, над С-библиотекой. Мы дадим нашему классу имя LINT (Long INTeger - длинное целое). Он будет содержать структуры данных и функции как компоненты с атрибутом открытый {public), который устанавливает возможность внешнего доступа. Доступ к структурам класса объявляется как частный (private) и, с другой стороны, может быть выполнен только функциями, которые являются членами класса или дружественными функциями. Функции - члены класса LINT могут получить доступ к функциям и элементам данных объектов класса LINT по имени. Требуются они для управления внешним интерфейсом класса, обслуживания запросов к нему, а также являются основными и вспомогательными для управления и обработки внутренних структур данных. Функции-члены класса LINT всегда могут вызвать LINT-объект через неявный левый аргумент (его нет в списке параметров). Функции - друзья класса не принадлежат самому классу, но, однако, они имеют доступ к внутренней структуре класса. В отличие от функций-членов, функции-друзья не содержат неявный аргумент. з Читатель может ознакомиться с несколькими традиционными работами по введению в язык С и его обсуждению, а именно: [ElSt], [Strl], [Str2], [Deit], [Lipp] здесь перечислено несколько Vй наиболее важных названий. За основу попыток стандартизации ISO был взят [ElSt], который &'< стандартом. 1
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 289 Объекты генерируются как экземпляры класса при помощи конструкторов, которые осуществляют распределение памяти, инициализацию данных и другие задачи управления перед тем как объект будет готов к выполнению работы. Нам потребуется несколько подобных конструкторов для того, чтобы сгенерировать LINT-объекты из различного контекста. Наряду с конструкторами существуют и деструкторы, занимающиеся удалением объектов, которые больше не нужны, и ресурсов, которые были им выделены. Вот элементы языка C++, которые мы обычно используем в разработке наших классов: • перегрузка операций и функций; • усовершенствованные возможности, по сравнению с С, для ввода и вывода. В следующих частях рассматривается применение этих двух принципов при разработке нашего класса LINT. Для того чтобы у читателя появилось представление о том, что из себя представляет класс LINT, мы покажем небольшую часть его объявления: // Конструктор // Деструктор ',',' const LINT& operator (const LINT&); ;Т const LINT& operator+= (const LINT&); \^ const LINT& operator^ (const LINT&); 2.» const LINT& operator*= (const LINT&); const LINT& operators (const LINT&); const LINT& operator%= (const LINT&); const LINT gcd (const LINT&); const LINT Icm (const LINT&); const Int jacobi (const LINT&); friend const LINT operator + (const LINT&, const LINT&); friend const LINT operator - (const LINT&, const LINT&); 10-1697 к: ;.ч class LINT { public: LINT (void); -LINT 0;
290 Криптография на Си и C++ в действии friend const LINT operator * (const LINT&, const LINT&); friend const LINT operator / (const LINT&, const LINT&); friend const LINT operator % (const LINT&, const LINT&); friend const LINT mexp (const LINT&, const LINT&, const LINT&); friend const LINT mexp (const USHORT, const LINT&, const LINT&); friend const LINT mexp (const LINT&, const USHORT, const LINT&); friend const LINT gcd (const LINT&, const LINT&); friend const LINT Icm (const LINT&, const LINT&); friend const int jacobi (const LINT&, const LINT&); private: dint *nj; int maxlen; int init; int status; }; Можно выделить в классе LINT типовое подразделение на два блока: первый, открытый блок с конструктором, деструктором, арифметическими операторами и функциями-членами и друзьями данного класса. Небольшой блок закрытых элементов данных присоединен к открытому интерфейсу и идентифицирован меткой private. Такое деление используется для большей ясности и хорошим тоном считается расположить открытый интерфейс перед закрытым блоком, а метки public и private использовать только один раз внутри каждого объявления класса. Приведенный здесь список операторов, который фигурирует в разделе объявления класса, очевидно, еще не совсем завершен. В нем не хватает некоторых арифметических функций, которые не могут быть представлены в качестве операторов, так же как и большинство теоретико-числовых функций, которые нам уже известны как функции языка С. Более того, объявленные конструкторы представлены так же мало, как и функции ввода и вывода объектов LINT. В следующем списке параметров операторов и функций появляется ссылочный оператор &, в результате применения которого объекты класса LINT передаются не по значению, а по указателю на объект. Аналогично происходит и при возврате. Такое использование # недопустимо в С. Однако при более доскональном рассмотрении 41
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 291 можно заметить, что только лишь определенные функции-члены возвращают указатель на объект LINT, в то время как многие другие возвращают в качестве результата само значение. Основным правилом, которое определяет, какой из двух методов используется, является то, что функции, изменяющие один или более аргументов, которые им передаются, могут вернуть результат в качестве ссылки, тогда как другие функции, не изменяющие входные параметры, возвращают результат как значение. По мере изучения мы увидим, в каких LINT-функциях используется какой способ. Классы в языке C++ являются расширением сложного типа данных «структура» в Си, и доступ к элементу х класса производится синтаксически точно так же, как и к элементу структуры, то есть, например, А.х, где А - указывает на объект, а х - на элемент класса. .>«'- Также следует отметить, что в списке параметров функции-члена ,{'* аргумент имеет неполное имя в отличие от точно так же названной •.. функции-друга, что показано в следующем примере: ./: friend LINT gcd (const LINT&, const LINT&); в сравнении с : '-l,i';l LINT LINT::gcd (const LINT&); J Поскольку функция gcd() в качестве функции - члена класса LINT принадлежит объекту А типа LINT, вызов gcd() должен происхо- :i/ ' Л дить в форме A.gcd(b) без появления А в списке параметров. Однако дружественная функция gcd() не принадлежит ни одному объекту и ц:.н ; % : таким образом не содержит неявных аргументов. * Мы наполним указанную выше упрощенную структуру нашего класса LINT в следующих главах и выявим множество ее особенностей, для того чтобы со временем у нас была окончательная реализация класса LINT. Те, кому интересно узнать в общем о C++, могут обратиться к следующим ссылкам: [Deit],[ElSt],[Lipp], a также [Meyl] и [Меу2]. 13.1. Частное дело: представление чисел в классе LINT Если мои идеи не похожи на их мнение, Оставлю лучше их при своей точке зрения. А. Е. Хаусман, Последние поэмы IX Представление длинных чисел, которые были выбраны для нашего класса, является расширением их представления на С, описанного в I части. Оттуда мы возьмем расположение цифр натурального числа как вектор значений типа dint, в котором наиболее старшие 10*
292 Криптография на Си и C++ в действии разряды располагаются по старшему индексу (см. главу 2). Память, которая требуется для этого, автоматически выделяется в момент генерации объекта. Этот процесс осуществляется конструкторами, которые вызываются как явно - в программе, так и неявно - при компиляции с помощью new(). Поэтому в объявлении класса нам потребуется переменная типа dint *n_l, с которой в рамках одного конструктора и ассоциируется указатель на размещаемую там, память. В качестве второго элемента нашего числового представления мы определяем переменную maxlen, которая хранит количество памяти, выделенной конструктором отдельному объекту. Переменная maxlen определяет максимальное число clint-разрядов, которые может иметь объект. Более того, мы хотим установить, был ли LINT-объект инициализирован, то есть было ли ему присвоено какое-нибудь числовое значение перед тем как он был использован справа от знака равенства в числовом выражении. Поэтому мы вводим переменную init целого типа, которая изначально имеет значение 0 и устанавливается в 1, когда впервые объекту присваивается числовое значение. Мы реализуем наши функции и операторы класса LINT так, чтобы сообщение об ошибке выдавалось в случае, если не определены значения объекта LINT, а, как следствие, и значение выражения. Переменная status, строго говоря, не является элементом нашего представления в числовом виде. Ее применяют для индикации ситуации переполнения или потери значимости (см. стр. 32), если оно происходит в результате выполнения операций над объектами LINT. Типы и механизмы сообщений об ошибках и обработка ошибок подробно описаны в главе 15. Таким образом, класс LINT определяет совокупность следующих элементов для представления целых чисел и хранения состояний объектов: dint* nj; int init; int maxlen; int status; Так как мы имеем дело с закрытыми элементами, доступ к этим элементам класса возможен только лишь посредством функций- членов или функций-друзей, а так же таких же операторов. В частности, не существует возможности прямого доступа к отдельным разрядам числа, которое было представлено объектом класса LINT.
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 293 13.2. Конструкторы. Конструкторы - это функции для генерации объектов определенного класса. Для класса LINT это может происходить как с инициализацией, так и без нее, причем в последнем случае объект будет создан и требуемая для хранения числа память выделена, однако никакого значения объекту не будет присвоено. Конструктору для этого не нужно никаких аргументов и таким образом он играет роль конструктора по умолчанию для класса LINT (см. [Strl], Раздел 10.4.2). Следующий конструктор по умолчанию LINT(void) в файле flintpp.cpp создает объект LINT без присвоения ему значения: LINT::LINT (void) { n_l = new CLINT; if (NULL == nj) { panic (E_LINT_NHP, "конструктор 1", 0, _LINE_); } maxlen = CLINTMAXDIGIT; init = 0; status = E_LINTJDK; } Если заново порожденный объект инициализируется числовым значением, то подходящий конструктор должен способствовать генерации объекта LINT, а затем присваивать ему заранее определенный аргумент в качестве значения. В зависимости от типа аргумента должны быть введены несколько перегружаемых конструкторов. Класс LINT содержит функции-конструкторы, которые показаны в таблице 13.1. Теперь мы хотим обсудить следующий пример конструктора LINT вида LINT (const char* const), который генерирует объекты LINT и ассоциирует с ними значение, взятое из строки ASCII-символов. В строке может быть префикс, который содержит информацию об основании числового представления. Если строка символов начинается с Ох или ОХ, то ожидаются наборы символов в шестнадцатиричном виде из диапазонов {0,1,...,9}, {a,b,...,f} и {A, B,...,F}. Если префиксом является 0Ь или 0В, то следует ожидать появления бинарных разрядов из множества {0,1}. Если префикса не существует вовсе, то разряды интерпретируются как цифры в десятичном виде. Конструктор использует функцию str2clintj() для того, чтобы пре-
294 Криптография на Си и C++ в действии образовывать строку символов в объект типа CLFNT, из которого на втором шаге будет создан объект LINT: LI NT:: LI NT (const char* const str) { nj = new CLINT; if (NULL == nj) // ошибка в new? { panic (EJJNTJMHP, "конструктор 4", 0, _LINE_); } if (strncmp (str, "Ox", 2) == 0 || strncmp (str, "OX", 2) == 0) { int error = str2clint_l (nj, (char*)str+2,16); } else { if (strncmp (str, "Ob", 2) == 0 || strncmp (str, "OB", 2) == 0) { error = str2clintj (nj, (char*)str+2, 2); } else { error = str2clint_l (nj, (char*)str, 10); } } switch (error) // оценка кода ошибки { case 0: maxlen = CLINTMAXDIGIT; init = 1; status = E_LINT_OK; break; caseE CLINT_BOR:
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 295 panic (E_L|NT_BOR," конструктор 4", 1, LINE ); break; case E_CL!NT_OFL: panic (EJJNTJDFL," конструктор 4", 1, LINE ); break; case E_CLINT_NPT: panic (E_LINT_NPT," конструктор 4м, 1, _LINE_); break; default: panic (EJJNTJERR," конструктор 4", error, LINE V, } Таблииа 13.1. Конструкторы LINT T4t- lip- Jo J J'C: * • ill Г Конструктор LINT (void); LINT (const char* const, const unsigned char); LINT (const UCHAR* const, const int) LINT (const char* const); LINT (const LINT&); LINT (const int); LINT (const long int); LINT (const UCHAR); LINT (const USHORT); LINT (const unsigned int); LINT (const ULONG); LINT (const CLINT); Семантика: создание объекта LINT Без задания начального значения (конструктор по умолчанию) Из строки символов с основанием числового представления, заданной во втором аргументе Из вектора байтов с длиной, заданной во втором аргументе Из строки символов, возможно с префиксом ОХ для шестнадцатиричных чисел или ОВ для бинарных Для других объектов LINT (конструктор копирования) Из значения типа char, short или int Из значения типа long int Из значения типа UCHAR Из значения типа USHORT Из значения типа unsigned int Из значения типа ULONG Из объекта CLINT Конструкторы позволяют проводить инициализацию объектов LINT другими такими объектами, так же как и стандартными типами, константами и символьными строками, что и показано на следующем примере:
296 Криптография на Си и C++ в действии LINT a; LINT one (1); int t = 2147483647; UNTb(i); LINT с (one); LINT d ("0x123456789abcdef0"); Функции-конструкторы вызываются явно для генерации объектов типа LINT из заданных аргументов. Конструктор LINT, который, например, преобразует значения типа unsigned long в объекты LINT, реализован в следующей функции: LINT::LINT (const ULONG ul) { nj = new CLINT; if (NULL == nj) { panic (EJJNTJ4HP, "Конструктор 11", 0, _LINE_); } ul2c(intj (nj, ul); maxlen = CLINTMAXDIGIT; init = 1; status = E_LINT_OK; } А теперь нам необходимо получить функцию-деструктор, которая соответствует конструкторам класса LINT и которая дает возможность освободить объекты, в частности, связанную с ними память. Вообще говоря, компилятор охотно создает деструктор по умолчанию, которым можно пользоваться, однако он освобождает только память, которую занимают элементы объекта LINT. Дополнительная память, которая выделяется конструктами, не освобождается, в результате чего идет утечка памяти. Следующий далее короткий деструктор выполняет важную задачу освобождения памяти, занимаемой объектами LINT. ~LINT() { delete [] nj;
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 297 13-3. Перегрузка операторов. Перегрузка операторов представляет собой достаточно мощный механизм, позволяющий определить функции с одинаковыми именами, но с различным списком параметров, то есть которые могут выполнить отличающиеся операции. Компилятор использует специальный список параметров для того, чтобы определить, какая функция имеется в виду. Чтобы сделать это, C++ использует строгий контроль типов, который позволяет избежать двусмысленности или противоречивости. Перегрузка фукнций-операторов позволяет применять "обычный" способ записи выражения суммы с = а + b с объектами a, b и с клас- са LINT вместо того, чтобы вызвать функцию, например, addj(a_l, _ b_l, cj). Это позволяет осуществить органичную стыковку класса с ^ языком программирования, а также значительно повышает читабельность программ. Для данного примера необходимо перегрузить оператор "+" и присваивание "=". Существует всего несколько операторов в C++, которые нельзя перегружать. Даже оператор "[ ]" который применяется для получения доступа к векторам, может быть перегруженным, например, функцией, одновременно проверяющей, не превышают ли запрошенный индекс вектора его границ. Однако не нужно забывать, что перегрузка операторов открывает путь к возможным неприятностям. В частности, не могут быть изменены операторы над стандартными типами данных; так же не может быть изменен заранее определенный приоритет операторов (см. [Strl], Раздел 6.2) или "созданы" новые операторы. Но для отдельных классов вполне возможным является оп- "~~~ "'- ределение функции-оператора, не имеющего ничего общего с тем ** «W», оператором, с которым он обычно ассоциировался. Для того, чтобы в программах было проще разобраться, необходимо следовать следующему совету - строже придерживаться смысла стандартных операторов в C++ при перегрузке, чтобы избежать бесполезной путаницы. Следует отметить, что в структуре класса LINT, отмеченной выше, некоторые операторы выполнялись как функции-друзья, а другие как функции-члены. Причиной этому послужило то, что мы хотели — - бы использовать, например, "+" или "*" в двух качествах: когда они ттитт*т*м могут не только обрабатывать два эквивалентных объекта LINT, но и, как альтернатива, принять один объект LINT и один из встроенных целочисленных типов языка C++, более того, принять аргументы в любом порядке, поскольку сложение является коммутативным. Для этой цели нам потребуются ранее описанные конструкторы, которые создают объекты LINT из целых типов. Комбинированные выражения, такие как: LINT a, b, с; int number;
298 Криптография на Си и C++ в действии Таблица 13.2. Арифметические операторы класса LINT Таблица 13.3. Побитовые операторы ^класса LINT II Инициализация a, b и number и какие-то вычисления /I... с = number * (а + b / 2) также становятся возможными. В обязанности компилятора входит автоматический вызов подходящих функций конструктора. Он также следит за тем, чтобы преобразование целого числа number и константы 2 в объекты LINT происходило в момент выполнения программы, перед тем как будут вызваны операторы + и *. Таким образом мы получим максимально возможную гибкость в применении операторов с тем лишь ограничением, что выражения, содержащие объекты типа LINT, сами являются типом LINT и, соответственно, могут быть присвоены только объектам типа LINT. Перед тем как мы углубимся в детали каждого отдельного оператора, нужно получить общее представление об операторах, определенных классом LINT, которые читатель может найти в Таблицах 13.2-13.5. + ++ - -- * / % Сложение Инкремент (префиксный и постфиксный) Вычитание Декремент (префиксный и постфиксный) Умножение Деление (частное) Остаток & « » Побитовое И Побитовое ИЛИ Побитовое исключающее ИЛИ (XOR) Сдвиг влево Сдвиг вправо Таблица 13.4. Логические операторы класса LINT Равенство Неравенство Меньше, меньше или равно Больше, больше или равно
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 299 Таблииа 13.5. Операторы присваивания класса LINT 01 • -же = += -= *_ /= %= &= 1= л= «= »= Простое присваивание Присваивание после сложения Присваивание после вычитания Присваивание после умножения Присваивание после деления Присваивание после взятия остатка Присваивание после побитового И Присваивание после побитового ИЛИ Присваивание после побитового исключающего ИЛИ Присваивание после левого сдвига Присваивание после правого сдвига МИШ Сейчас мы хотим обсудить реализацию функций операторов "*", »_>^ »*=>^ »—>^ КОТОрЫе могут служить примерами реализации операторов LINT. Сначала с помощью оператора "*" можно увидеть, как выполняется умножение объектов LINT с помощью функции С mulj(). Оператор реализован как функция-друг, в которую оба сомножителя передаются по ссылке. Так как функции-операторы не меняют своих аргументов, ссылки объявляются как const (константы): const LINT operator* (const LINT& lm, const LINT& In) { LINT prd; int error; Mir ( ] m ) \ :{ 1 Сначала нужно убедиться, инициализированы ли аргументы lm и In, переданные по ссылке. Если это не выполняется для обоих аргументов, то включается обработка ошибок и вызывается функ- иия-член panicO, которая объявлена как статическая (см. Главу 15) if (Um.init) LINT::panic (E_LINT_VAL, "*", 1, _LINE_); if (lln.inlt) LINT::panic (E_LINT_VAL, "*", 2, _LINE_); Вызывается С-функиия mul_l(), в которую передаются в качестве аргументов: векторы Im.nJ, In.nJ - как множители, a prd.nj - как результат, куда будет помешено произведение. error = mulj (Im.nJ, In.nJ, prd.nj);
300 Криптография на Си и C++ в действии При оценке кола ошибки, который хранится в переменной error, возможны 3 случая. Если error == 0, то все в порядке и объект prd может быть отмечен как инициализированный. Это делается присваиванием переменной prd.init 1 .Переменная статуса prf.status уже была установлена конструктором в значение E_LINT_OK. Если произошло переполнение в функции mul_l(), то переменная error содержит значение E_CLINT_OFL. Поскольку в этом случае вектор prd.nj содержит правильное по формату число CLINT, то prd.init установлен на 1, в то время как переменная статуса prd.status содержит значение E_LINT_OFL Если error не содержит ни одного из этих двух значений после вызова mul_l(), то что-то , в этих функциях прошло не так, причем мы не имеем возможным определить более точно, какую именно ошибку мы сделали. В этом случае вызывается функция panic() для дальнейшей обработки ошибки. switch (error) { case 0: prd.init =1; break; case E_CLINT_OFL: prd.status = E_LINT_OFL; prd.init =1; break; default: LINT::panic (E_LINT_ERR, "*", error, _LINE_); } f ) Если ошибка не может быть исправлена функцией panicO, то и возврат в эту точку не произойдет. Механизм распознавания ошибок здесь приведёт к завершению, которое, в принципе, лучше, чем продолжение работы программы в неопределенном состоянии. И как заключающий шаг - поэлементный возврат произведения prd. return prd; Поскольку объект prd существует лишь внутри контекста функций, компилятор обеспечивает автоматическое создание временного объекта, который представляет значение переменной prd вне функции. Данный временный объект создается с помощью конструктора
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 301 копирования LINT (const LINT&) (см. стр. 295) и существует до тех пор, пока выражение, в рамках которого использовался оператор, не будет обработано, то есть по достижении закрывающей точки с запятой. Так как значения функции объявлены как const, компилятор не воспримет такие бессмысленные конструкции как (а * Ь) = с. Основной целью здесь является работа с LINT-объектами теми же способами, как и со встроенными целых типов. tji. Мы можем расширить возможности функции-оператора, используя " :{ следующие приемы: если сомножители одинаковы, то умножение ti заменяется возведением в квадрат, и преимущество в скорости, ,%и i связанное с этой заменой, может быть применено автоматически -ж<** (см. п. 4.2.2). Однако придется затратить некоторые усилия на по- : ;<>•, элементное сравнение аргументов для того, чтобы определить их '•он-, равенство, что обходится нам дорого, и следует довольствоваться •дш компромиссом: возведение в квадрат следует выполнять только в случае, если оба сомножителя ссылаются на один и тот же объект. Таким образом, мы проверим, являются ли In и lm указателями на один и тот же объект, и в этом случае вместо умножения выполним возведение в квадрат. Ниже приведен соответствующий текст программы: if (&lm == &ln) { ^ error = sqr_l (Im.nJ, prd.nj); } f * else error = mulj (Im.nJ, In.nJ, prd.nj); } . Этот взгляд назад на функции, реализованные в языке С в части I, представляет собой модель для всех оставшихся функций класса LINT, который сформирован как оболочка вокруг ядра функций С и защищает его от пользователей класса. Прежде чем мы обратимся к более сложному оператору присваивания "*=", лучше, по-видимому, более подробно рассмотреть простой оператор присваивания "=". В части I мы уже определили, что присваивание объектов требует особого внимания (см. главу 8). Следовательно, как и в реализации на С, нужно четко обращать внимание на то, чтобы при присваивании одного объекта класса LINT другому присваивалось содержимое, а не адрес. Точно так же нам нужно для нашего класса LINT определить специальный вариант оператора присваивания "=", который выполняет не только простое копирование элементов класса: по тем же причинам, которые были описаны в главе 8, нам следует обратить внимание на то,
02 Криптография на Си и C++ в действии что копируется не адрес числового вектора n_J, а разряды, на кото- I рые он ссылается. I" Как только стало понятным основное требование к порядку дейст- I вий, дальнейшая реализация не будет очень сложной. Оператор "=" I реализован как функция-член и возвращает как результат ссылку Г на неявный левый элемент. Вне всяких сомнений, мы будем ис- [ пользовать внутри функцию С сру_1() для того, чтобы перенести I разряды из одного объекта в другой. Для того чтобы выполнилось I присваивание а = Ь, компилятор вызывает функцию-оператор "=" I в контексте а, которая берет на себя роль неявного аргумента, не I указанного в списке параметров функции-оператора. В рамках I функции-члена ссылка на элементы неявного аргумента может I быть выполнена просто по имени, без учета контекста. Более того, I ссылку на неявный объект можно сделать с помощью специального I указателя this, как показано в приведенном ниже примере реализа- I ции оператора "=": I I const LINT& LINT::operator= (const LINT& In) | { 1 if (Nn.init) panic (E_LINT_VAL, "=", 2, _LINE_); I if (maxlen < DIGITS_L (In.nJ)) I panic (E_LINT_OFL, "=", 1, _LINE_); I Сначала проверим, являются ли ссылки на правый и левый аргу- I менты одинаковыми, так как в этом случае нет необходимости ) I копировать. В противном случае, разряды числового представле- I ния In копируются в неявный левый аргумент *this, так же как I переменные init и status, а возвращается ссылка на неявный эле- I мент в виде *this. if (&ln != this) { cpyj (nj, In.nJ); init = 1; status = In.status; } return *this; } Возникает вопрос: нужно ли вообще оператору присваивания возвращать какое-нибудь значение, так как после вызова LINT:."operator = (const LINT&) нужное присваивание вроде бы сделано. Однако ответ
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 303 на данный вопрос достаточно очевиден, если вспомнить, что разрешено выражение в форме f(a = b); Согласно семантике языка C++, подобное выражение является результатом вызова функции f с результатом присваивания а = b в качестве аргумента. Таким образом, крайне необходимо, чтобы оператор присваивания возвращал значение как результат, и для большей эффективности это делается с помощью ссылки. Частным случаем такого выражения является а = b = с ; где оператор присваивания вызывается два раза подряд. Когда он вызывается второй раз, результат первого присваивания b = с присваивается а. В отличие от оператора "*", оператор "*=" меняет самый левый из двух сомножителей, записывая на его место значение произведения. Смысл выражения а *= b как сокращенная запись выражения а = а * Ь, вне всякого сомнения, должен остаться верным для объектов класса LINT. Следовательно, оператор "*=", как и оператор "=", может быть создан как функция-член, которая по причинам, обсужденным выше, возвращает ссылку на результат: const LINT& LINT::operator*= (const LINT& In) { int error; If (Unit) panic (EJJNT_VAL, "*=", 0, _LINE_); if (lln.init) panic (E_LINT_VAL, "*=", 1, _LINE_); if (&ln == this) error = sqrj (nj, nj); else error = mulj (nj, In.nJ, nj); switch (error) { case 0: status = E_LINT_OK;
304 Криптография на Си и C++ в действии break; case E_CLINT_OFL: status = EJJNTJDFL; break; default: panic (E_LINT_ERR, "*=", error, _LINE_); } return *this; } В качестве нашего последнего примера оператора LINT мы опишем функцию "= =", которая проверяет на равенство два объекта LINT: в результате возвращается значение 1, если они равны, и 0 - в противном случае. Оператор = = также иллюстрирует реализацию других логических операторов. const Int operator== (const LINT& Im, const LINT& In) у* '* if (Nm.init) LINT::panlc (E_LINT_VAL, "==", 1, _LINE_); if (Nn.init) LINT::panic (E_LINT_VAL, "==", 2, _LINE_); if (&ln == &lm) return 1; else return equj (Im.nJ, In.nJ);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса Пожалуйста, примите мою отставку. Я не хочу принадлежать какому-нибудь клубу, который то;■■■.■■<<} примет меня в качестве члена клуба. Гручо Маркс ДО ' ' и..,г Каждый раз, когда я пишу портрет, я теряю друга. Джон Сингер Сарджент В добавление к функциям-конструкторам и операторам, о которых говорилось ранее, существуют и дополнительные функции LINT, которые делают функции языка С, рассмотренные в части I, дос- ||у<: тупными объектам LINT. На повестке грядущего обсуждения ле- , '' жит следующее: мы сделаем приблизительное разделение функций г. на "арифметические" и "теоретико-числовые" категории. Реализацию функций мы обсудим вместе с примерами; в остальных случаях л,. мы ограничимся таблицей, которая требуется для их применения по назначению. Также в следующих разделах мы дадим трактование в более широкой форме функциям форматированного вывода объектов **"•" LINT, для которых мы воспользуемся свойствами классов stream, :^ содержащихся в стандартной библиотеке C++. Во многих руководи' дствах по языку C++ достаточно быстро расправляются со всевозможными приложениями, особенно по форматированному выводу объектов классов, которые определяются пользователем, и, пользуясь случаем, мы собираемся раскрыть конструкцию функций, необходимых для вывода наших объектов класса LINT. 14.1. Арифметика Следующие функции-члены выполняют основные арифметические операции, так же как и модульные вычисления в кольце классов —*...—^«,.„ * вычетов колец в режиме сумматора: объект, к которому принадлежит вызываемая функция, содержит результат выполнения функции как неявный аргумент после ее завершения. Функции-сумматоры являются эффективными, так как они работают преимущественно без внутренних вспомогательных объектов, таким образом экономя лишние присваивания и вызовы конструкторов.
306 Криптография на Си и C++ в действии В тех случаях, в которых присваивание результатов вычисления является неизбежным или же в которых автоматическая перезапись неявно выраженного аргумента функции-члена результатом является нежелательной, мы расширим их с помощью аналогичных функций-друзей с похожими названиями вместе с дополнительными функциями-друзьями. Мы не будем обсуждать это в дальнейшем в данной главе, однако упомянем об этом в приложении В. Интерпретация возможных ошибочных ситуаций в функциях LINT, которые могут возникнуть от применения функций CLINT, будет рассмотрена в полной мере в главе 15. Прежде чем мы составим список открытых функций-членов, нам предстоит рассмотреть пример их реализации в виде функции возведения в степень, для которой, увы, C++ не предоставляет никакого оператора. LINT& LINT::mexp (const LINT& е, const LINT& m); и LINT& LINT::mexp (const USHORT e, const LINT& m); Функции mexpQ построены так, что вызываемые ими С-функции являются оптимизированными в зависимости от типа операндов (а именно, mexpkJO, mexpkmj(), umexpJO или umexpmJO), а в соответствующих арифметических дружественных функциях мы будем обычно иметь дело с функциями возведения в степень wmexp_l() и wmexpmJO с основанием типа USHORT. Функция: Синтаксис: Вход: Возврат: Пример: Модульное возведение в степень с автоматическим применением возведения в степень Монтгомери, если модуль оказался нечетным. const LINT& LINT::mexp (const LINT& е, const LINT& m); неявный аргумент (основание) е (экспонента) m (модуль) указатель на остаток а.техр (е, т); 1 const L1NT& LINT::mexp (const LINT& е, const LINT& т) { int error; if (Mnit) panic (E_LINT_VAL, "mexp", 0, _LINE_);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 307 if (le.init) panic (EJJNT_VAL, "mexp", 1, _LINE_); if (Im.init) panic (E_LINT__VAL, "mexp", 2, _LINE_); err = mexpj (n_l, e.nj, nj, m.nj); /* mexpJ() использует mexpkJO или mexpkmJO */ switch (error) { case 0: status = E_LINT_OK; break; case E_CLINT_DBZ: panic (EJJNTJDBZ, "mexp", 2, _LINE_); break; default: panic (EJJNT_ERR, "mexp", error, _LINE_); } return *this; } Функция: Синтаксис: Пример: Модульное возведение в степень constLINT& LINT::mexp (const USHORT e, const LINT& m); a.mexp (e, m); const LINT& LINT::mexp (const USHORT e, const LINT& m) { int err; if (!init) panic (E_LINT_VAL, "mexp", 0, _LINE_); if (Im.init) panic (E_LINT_VAL, "mexp", 1, _LINE_); err = umexpj (nj, e, nj, m.nj);
308 Криптография на Си и C++ в действии switch (err) { // Здесь код, аналогичный вышеприведенному в техр (const LINT& е, const LINT& т) } return *this; } i А теперь мы рассмотрим набор дополнительных арифметических и теоретико-числовых функций-членов. Функция: Синтаксис: Вход: Возврат: Пример: сложение const LINT& LINT::add (const LINT& s); неявный аргумент (слагаемое) s (слагаемое) указатель на сумму a.add (s); выполняет операцию а += s; Функция: Синтаксис: Вход: Возврат: Пример: вычитание const LINT& LINT::sub (const LINT& s); неявный аргумент (уменьшаемое) s (вычитаемое) указатель на разность a.sub (s); выполняет операцию а -= s;
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 309 Функция: Синтаксис: Вход: Возврат: Пример: умножение const LINT& const LINT& LINT::mul (const LINT& s); неявный аргумент (множитель) s (множитель) указатель на произведение a.mul (s); выполняет операцию а *= s; Функция: возведение в квадрат Синтаксис: const LINT& LINT::sqr (void); Вход: неявный аргумент (множитель) Возврат: указатель на неявный аргумент, который содержит квадрат Пример: a.sqr (s); выполняет операцию а *= а; Функция: деление с остатком Синтаксис: const L1NT& LINT::divr (const LINT& d, LINT& г); Вход: неявный аргумент (делимое) d (делитель) Выход: г (остаток от деления по модулю d) Возврат: указатель на неявный аргумент, который содержит частное Пример: a.divr (d, г); выполняет операцию а /= d; г = а % d; it
310 Криптография на Си и C++ в действии Функция: Синтаксис: Вход: Возврат: Пример: нахождение остатка const LINT& LINT::mod (const LINT& d); неявный аргумент (делимое) d (делитель) указатель на неявный аргумент, который содержит остаток от деления по модулю d a.mod (d); выполняет операцию а %= d; Функция: Синтаксис: Вход: Возврат: Пример: Примечание: нахождение остатка по модулю степени двойки const LINT& LINT::mod2 (const USHORT e); неявный аргумент (делимое) е (показатель степени делителя) указатель на неявный аргумент, который содержит остаток от деления по модулю 2е a.mod 2(e); выполняет операцию а %= d, где d=2e mod2 не может быть создана с помощью перегрузки ранее представленной функции mod(), потому что mod() так же принимает аргумент USHORT, который автоматически приводится в объект LINT посредством подходящего конструктора. Поэтому из аргументов не ясно, какая функция имеется в виду, mod2() дается свое собственное имя. J Функция: Синтаксис: Вход: Возврат: Пример: проверка на равенство по модулю m const int i LINT::mequ (const LINT& b, const LINT& m); неявный аргумент а второй аргумент b модуль m 1, если а = b mod m, в противном случае 0 if (a.mequ (b, m)) //...
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 311 функция: Синтаксис: Вход: Возврат: Пример: модульное сложение const LINT& LINT::madd (const LINT& s, const L1NT& m); неявный аргумент (слагаемое) s (слагаемое) m (модуль) указатель на неявный аргумент, который содержит сумму по модулю m a.madd (s, m); Функция: Синтаксис: Вход: I Возврат: Пример: модульное вычитание const LINT& LINT::msub (const LINT& s, const LINT& m); неявный аргумент (уменьшаемое) s (вычитаемое) m (модуль) указатель на неявный аргумент, который содержит разность по модулю m a.msub (s, m); Функция: модульное умножение Синтаксис: const LINT& LINT::mmul (const LINT& s, const LINT& m); Вход: неявный аргумент (множитель) s (множитель) m (модуль) возврат: указатель на неявный аргумент, который содержит произведение по модулю m Пример: a.mmul (s, m);
312 Криптография на Си и C++ в действии Функция: Синтаксис: Вход: Возврат: Пример: модульное возведение в квадрат const LINT& LINT::msqr (const LINT& m); неявный аргумент (множитель) г m (модуль) v указатель на неявный аргумент, который содержит квадрат по модулю m a.msqr (m); Функция: Синтаксис: Вход: Возврат: Пример: модульное возведение в степень с показателем вида 2е const LINT& LINT::mexp2 (const USHORT e, const LINT& m); неявный аргумент (основание) e (степень 2) m (модуль) указатель на неявный аргумент, который содержит степень по модулю m a.mexp2 (e, m); Функция: Синтаксис: Вход: Возврат: Пример: модульное возведение в степень (2к-арный метод, с преобразованием Монтгомери) const LINT& LINT::mexpkm (const LINT& е, const LINT& m); неявный аргумент (основание) е (показатель) m (нечетный модуль) указатель на неявный аргумент, который содержит степень по модулю m a.mexpkm (e, m); |
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 313 Функция: Синтаксис: Вход: ш.г-,. Возврат: Пример: модульное возведение в степень (25-арный метод, с преобразованием Монтгомери) const LINT& LINT::mexp5m (const LINT& е, const LINT& m); неявный аргумент (основание) е (показатель) • m (нечетный модуль) указатель на неявный аргумент, который содержит степень по модулю m а.техрбт (е, т); Функция: Синтаксис: Вход: Возврат: Пример: левый/правый сдвиг const LINT& LINT::shift (const int noofbits); неявный аргумент (множимое/делимое) (+/-) noofbits (число позиций, на сколько должны быть сдвинуты биты) указатель на неявный аргумент, который содержит результат операции сдвига a.shift (512); выполняет операцию а «= 512; Функция: Синтаксис: Вход: Возврат: Пример: проверка делимости на 2 объекта LINT const int LINT::iseven (void); кандидат а как неявный аргумент 1, если а - нечетное, в противном случае 0 if(a.iseven ()) //...
314 Криптография на Си и C++ в действии Функция: установка двоичного разряда объекта LINT в 1 Синтаксис: const LINT& LINT::setbit (const unsigned int pos); Вход: неявный аргумент а pos - позиция того бита, который будет установлен (начиная с 0) Возврат: указатель на а, с установленным битом в позиции pos Пример: a.setbit (512); Функция: Синтаксис: Вход: Возврат: Пример: проверка двоичного разряда объекта LINT const int LINT::testbit (const unsigned int pos); неявный аргумент а pos - позиция того бита, который будет проверен (начиная с 0) 1, если бит находящийся в позиции pos установлен, в противном случае 0 if(a.testbit (512)) //... Функция: установка двоичного разряда объекта LINT в 0 Синтаксис: const LINT& LINT::clearbit (const unsigned int pos); Вход: неявный аргумент а pos - позиция того бита, который будет установлен (начиная с 0) Возврат: указатель на а с нулевым битом в позиции pos Пример: a.clearbit (512);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 315 функция: обмен значениями двух объектов LINT Синтаксис: const LINT& LINT::fswap (LINT& b); Вход: неявный аргумент а второй аргумент b (значение, которое будет меняться на а) Возврат: указатель на неявный аргумент со значением b Пример: a.fswap (b); а и b обмениваются значениями 14.2. Теория чисел В отличие от арифметических функций, следующие теоретико- числовые функции-члены не перезаписывают первый неявный аргумент с результатом. Причиной этому является то, что при использовании более сложных функций, как показала практика, обычно не нужно перезаписывать аргумент, как это происходит в случае с простыми арифметическими функциями. Результаты следующих функций соответственно возвращаются как значения, а не как указатели. Функция: вычисление наибольшего целого, меньшего или равного логарифму по основанию 2 от объекта LINT Синтаксис: ' const unsigned int LlNT::ld (void); Вход: неявный аргумент а Возврат: целая часть log2 a Пример: i = a.ld ();
316 Криптография на Си и C++ в действии Функция: Синтаксис: Вход: Возврат: Пример: вычисление наибольшего общего делителя двух объектов LINT const LINT LINT::gcd (const LINT& b); неявно выраженный аргумент а второй аргумент b НОД(а, b) с = аГдсб (b); Функция: Синтаксис: Вход: Возврат: Пример: вычисление обратного значения по модулю п const LINT LINT::inv (const LINT& n); неявный аргумент а модуль n обратная величина по модулю п (если результат равен нулю, то gcd(a, n) >1 и инверсии не происходит) с = a.inv (b); Функция: наибольший общий делитель чисел а и b и его представление g = ua+vb в виде линейной комбинации а и b Синтаксис: const LINT LINT::xgcd (const LINT& b, LINT& u, int& sign_u, LINT& v, int& sign_v); Вход: неявный аргумент а, второй аргумент b Выход: множитель и в представлении НОД(а, Ь) sign_u - знаки множитель v в представлении НОД(а, Ь) slgn_v - знак v Возврат: НОД(а, Ь) Пример: g = a.xgcd (b, u, sign_u, v, sign_v);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 317 Функция: Синтаксис: Вход: Возврат: Пример: наименьшее общее кратное двух объектов LINT. const LINT LINT::lcm (const LINT& b); неявный аргумент а, второй аргумент b HOK (a, b) с = a.lcm (b); Функция: Синтаксис: Вход: Возврат: Пример: решение системы линейных сравнении х = a mod m, х = b mod n const LINT LINT::chinrem (const LINT& m, const LINT& b, const LINT& n); неявный аргумент а, модуль m, аргумент b, модуль п решение х системы сравнений, если все в порядке (Get_Warning_Status() == E_L1NT_ERR означает, что произошло переполнение или сравнения не имеют общего решения) х = a.chinrem (m, b, n); Функция-друг chinrem (const int noofeq, LINT** coeff) получает coeff - вектор указателей на объекты LINT, которые передаются как коэффициенты а]} mlt ci2, т2> а3, т3> ... системы линейных сравнений х^ modmh i=l, ..., noofeq (см. Приложение В) Функция: вычисление символа Якоби двух объектов LINT Синтаксис: const int LINT::jacobi (const LINT& b); **x<№ неявный аргумент а, аргумент b возврат: Символ Якоби от двух входных значений Пример: i = a.jacobi (b);
318 Криптография на Си и C++ в действии Функция: Синтаксис: Вход: Возврат: Пример: вычисление целой части от квадратного корня const LINT LINT::root (void); неявный аргумент а целая часть от квадратного корня с = a.root (); Функция: Синтаксис: Вход: Возврат: Пример: вычисление квадратного корня по модулю простого числа р const LINT LI NT::root (const LI NT& р); неявный аргумент а, простой модуль р > 2 квадратный корень из а, если а - квадратичный вычет по модулю р, в противном случае 0 (Get_Warning_Status() == E_LINT_ERR показывает, что а не является квадратичным вычетом по модулю р) с = a.root (p); Функция: Синтаксис: Вход: Возврат: Пример: вычисление квадратного корня объекта LINT по модулю а простого произведения р • q const LINT LINT::root (const LINT& p, const LINT& q); неявный аргумент а простой модуль р > 2, простой модуль q > 2 квадратный корень из а, если а - квадратичный вычет по модулю рч"> в противном случае 0 (Get_Warning_Status() == E_LINT_ERR показывает, что а не является квадратичным вычетом по модулю р*чО с = a.root (p, q); J
ГЛАВА 14. Открытый интерфейс Li NT: члены и друзья класса 319 Функция: Синтаксис: Вход: Возврат: Пример: проверка, является ли объект LINT квадратом const int LINT::issqr (void); кандидат а как неявный аргумент квадратный корень из а, если а является квадратом, в противном случае 0, если а == 0 или а не является квадратом if(0 == (г = a.issqr ())) //... Функция: Синтаксис: Вход: Возврат: Пример: вероятностная проверка объекта LINT на простоту const int LINT::isprime (void); кандидат а как неявный аргумент 1, если а "вероятно" простое, в противном случае 0 if(a.isprime ()) //... Функция: Синтаксис: Вход: Выход: Возврат: Пример: Разложение CLINT-объекта в виде а = 2еЬ const int LINT::twofact (LINT& b); неявный аргумент а b (нечетная часть а) показатель степени четной части а е = a.twofact (b);
320 Криптография на Си и C++ в действии 14.3. Потоковый ввод/вывод объектов LINT Классы, содержащиеся в стандартных библиотеках языка C++, такие как istream и ostream, являются абстракциями устройст ввода/вывода, полученных из базового класса ios. Класс iostream в свою очередь, получен из istream и ostream, и он позволяет писат и читать из них объекты1. Ввод и вывод происходит с помощь операторов поместить (insert) и извлечь (extract) "«" и "»' (см. [Teal], глава 8). Они возникают при перегрузке операторо сдвига, например, в выражении ostream& ostream::operator« (int i); istream& istream::operator» (int& i); в котором они разрешают вывод и ввод, соответственно, целы значений через следующие выражения: cout « i; cin »i; В качестве специальных объектов классов ostream и istream, именно cout и cin, выступают те же самые абстрактные файлы, ка объекты stdout и stdin стандартной библиотеки С. Применение потоковых операторов "«" и "»" для ввода и вывода отменяет необходимость вникать в подробности используемой аппаратуры. В этом нет ничего особенного, к примеру, функция С printf() ведет себя таким же образом: команда printf() должна всегда, в независимости от платформы, возвращать одинаковый результат. Однако лежащие за пределами изменяющегося синтаксиса, который ориентирован на образное отображение вставки объектов в поток, преимущества реализации потоков языка C++ лежат в строгой проверке типов, которая в случае с printf() возможна лишь отчасти, и своей собственной расширяемости. В частности, мы воспользуемся последним свойством, перегрузив операторы "«" и ">> для того, чтобы они поддерживали ввод и вывод объектов LINT. Завершим класс LINT следующими потоковыми операторами: friend ostream& operator« (ostream& s, const LINT& In); friend fstream& operator« (fstream& s, const LINT& In); Мы применяем это имя потоковых классов как синоним к тем, которые сейчас используются стандартной библиотеке C++, в которой имена класса, известные ранее, получили префикс basic^- Настройка этого идет в самой стандартной библиотеке, в которой к ранее использовавшимся именам классов можно обращаться через соответствующие объявления типов (typedef) (см. [KLSch]» Глава 12)
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 321 friend ofstream& operator« (ofstream& s, const LINT& In); friend fstream& operator» (fstream& s, LINT& In); friend ifstream& operator» (ifstream& s, LINT& In); Простая реализация перегрузки оператора "«" для вывода объектов LINT могла бы выглядеть приблизительно следующим образом: ****■• #include <iostream.h> ostream& operator« (ostream& s, const LINT& In) { if (Hn.init) LINT::panic (E_LINT_VAL, "ostream operator«", 0, _LINE_); s « xclint2str (In.nJ, 16, 0) « endl; s « Id (In) « " bit" « endl; return s; } Таким образом, оператор "«" определяет выводы разрядов объекта LINT как шестнадцатиричных значений, добавляя при этом двоичную длину числа на отдельной строке. В следующем разделе мы рассмотрим возможности выражения появления вывода объектов LINT с помощью функций форматирования, а также мы рассмотрим применение манипуляторов для того, чтобы вывод был настраиваемым. 14.3.1. Форматированный вывод объектов LINT. В данном разделе мы воспользуемся базовым классом ios стандартной библиотеки C++ и его функциями-членами для того, чтобы оп- •/*,, ,r, ределить наши собственные функции форматирования, подходящие ;л хы? для LINT, с целью управления форматом вывода объектов LINT. Далее мы создадим манипуляторы, которые организуют настройку формата вывода объектов LINT настолько просто, как это сделано для стандартных типов, определенных в C++. Ключевым моментом в создании форматированного вывода объектов LINT является возможность установки спецификаций формата, которые будут обрабатываться оператором "«". Мы рассмотрим механизм, обеспечиваемый классом ios (для более подробного рассмотрения см. [Teal], Глава 6, и [Р!а2], Глава 6), у которого функция- 11 _1KQ7
322 Криптография на Си и C++ в действии член xallocQ в объектах классов, производных от ios, выделяет переменную состояния типа long и возвращает ее индекс того же типа. Этот индекс хранится в переменной flagsindex. С помощью нее функцию-член ios::iword() можно использовать для того, чтобы получить доступ по чтению и записи к выделенной переменной управления выводом (иначе, переменной состояния) (см. [Р1а2], стр. 125). Для полной уверенности, что это происходит до вывода объекта LINT, мы определяем в файле flintpp.h класс Lintlnit следующим образом: class Lintlnit { public: Lintlnit (void); }; Lintlnit::Lintlnit (void) { // получить индекс к переменной состояния в классе ios LINT::flagsindex = ios::xalloc(); // установить состояние по умолчанию в cout и сегг cout.iword (LINT::flagsindex) = cerr.iword (LINT::flagsindex) = LINT::lintshowlength|LINT::linthex|LINT::lintshowbase; } Класс Lintlnit в качестве своего единственного элемента содержит конструктор Lintlnit::Lintlnit(). К тому же в классе LINT мы определяем член элемента данных setup типа Lintlnit, который инициализирован с помощью конструктора Lintlnit::Lintlnit(). Вызов xalloc() происходит во время инициализации, а таким образом выделяемая переменная состояния определяет стандартный формат вывода объектов LINT. Далее мы покажем часть объявления класса LINT, которая содержит объявление Lintlnit() как друга класса LINT, а также объявление переменных flagsindex, setup и различных переменных состояния в виде перечислимого типа (епшп): class LINT { public: //... enum { I
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 323 lintdec = 0x10, lintoct = 0x20, linthex = 0x40, lintshowbase = 0x80, MH lintuppercase =0x100, ^ " lintbin =0x200, lintshowlength = 0x400 }; и... * -• friend Lintlnit::Lintlnit (void); //... private: ^ //... static long flagsindex; static Lintlnit setup; II... }; Задание переменной setup как static означает, что эта переменная существует только один раз для всех объектов LINT, и, таким образом, связанный конструктор Lintlnit() вызывается только один раз. Здесь мы хотим ненадолго остановиться и рассмотреть всю проделанную нами работу. Установку формата вывода можно с тем же успехом организовать с помощью переменной состояния, с которой как с членом класса LINT намного проще было бы иметь дело. Основным преимуществом метода, который мы выбрали, является то, что вывод формата можно установить для каждого потока вывода отдельно и независимо друг от друга, (см. [Р1а2], страница 125), чего нельзя сделать, используя внутреннюю для класса LINT переменную состояния. Это организуется с помощью возможностей класса ios, механизмы которого нам пригодятся для подобных целей. Теперь, после того как были рассмотрены необходимые замечания, мы сможем определить функции состояния как члены класса LINT. Они отражены в Таблице 14.1. Мы рассмотрим пример реализации функций состояния для функции LINT::setf(), которая возвращает текущее значение состояния переменной типа long с ссылкой на поток вывода:
324 Криптография на Си и C++ в действии Таблица 14.1 Функции- состояния класса LINT и результат их лействия Функция состояния static long UNT::flags (void); static long LINT::flags (ostream&); static long LINT::setf(long); static long LINT::setf (ostream&, long); static long LINT::unsetf(long); static long LINT::unsetf (ostream&, long); static long LINT::restoref (long); static long LINT::restoref (ostream&, long); Пояснения Считывает переменную состояния, относящуюся к cout Считывает переменную состояния, относящуюся к заданному потоку вывода Устанавливает отдельные биты переменной состояния cout и возвращает предыдущее значение Устанавливает отдельные биты переменной состояния заданного потока и возвращает предыдущее значение Восстанавливает отдельные биты переменной состояния cout и возврашае-щ| предыдущее значение шЯ Восстанавливает отдельные биты |Н переменной состояния заданного потока ™ и возвращает предыдущее значение Устанавливает переменную состояния cout и возвращает предыдущее значение Устанавливает переменную состояния заданного потока и возвращает предыдущее значение long LINT::setf (ostream& s, long flag) { long t = s.iword (flagsindex); // Флаги для основания числового представления взаимоисключающие: if (flag & LINT::lintdec) { s.iword (flagsindex) = (t & ~LINT::linthex & ~LINT::lintoct & ~LINT::lintbin) | LINT::lintdec; flag Л= LINT::lintdec; } if (flag & UNT::linthex) {
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 325 s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::Iintoct & ~LINT::lintbin) | LINT::linthex; flag л= LINT::linthex; } if (flag & LINT::lintoct) { s.iword (flagsindex) = (t & ~LINT::)intdec & ~LINT::linthex ^ & -LINT::lintbin) | LINT::lintoct; ■w. flag л= LINTr.iintoct; } if (flag & LINT::lintbin) { s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::lintoct & ~LINT::linthex) | LINT::lintbin; flag л= LINT::lintbin; } // Все остальные флаги являются взаимно совместимыми s.iword (flagsindex) |= flag; return t; С помощью этой и оставшихся функций в Таблице 14.1 мы можем далее определить разные форматы вывода. Первоначально стандартный формат вывода представляет собой значение объекта LINT как шестнадцатиричное число в виде строки символов, которая ^ ,. /0 занимает столько строк на экране, сколько требуется для вывода всех цифр объекта LINT. В добавочной строке число цифр объекта LINT размещено слева от края. Были созданы следующие дополнительные режимы вывода объекта LINT: 3<rdo i i Основание представления цифр. Стандартным представлением цифр объектов LINT является шестнадцатиричное, а представлением длины - десятичное. Такие умолчания для объектов LINT могут быть установлены для стандартного потока вывода cout с определенным основанием системы счисления base с помощью вызова
B26 Криптография на Си и C++ в действии LINT::setf(LINT::base); а на заданный поток LINT::setf (ostream, LINT::base); где base может принимать одно из значений: linthex, lintdec, lintoct, lintbin которые обозначают соответствующий формат вывода. Например, вызов LINT::setf(lintdec) устанавливает вывод формата цифр в десятичной форме. Основание системы счисления для представления длины может быть задано функцией ios::setf (ios::iosbase); с iosbase = hex, dec, oct. 2. Отображение префикса для числового представления Для объекта LINT установкой по умолчанию является отображение с префиксом, показывающим как он представлен. Следующие вызовы L!NT::unsetf (LINT::lintshowbase); LINT::unsetf (ostream, LINT::lintshowbase); меняют эту установку. 3. Отображение шестнадцатиричных цифр в верхнем регистре По умолчанию установлено отображение шестнадцатиричных цифр и префикса Ох для шестнадцатиричного представления в нижнем регистре a b с d e f. Однако вызов LINT::setf (LINT::lintuppercase); LINT::setf (ostream, LINT::lintuppercase); меняет их для того, чтобы они превратились в префикс ОХ и боль шие буквы А В С D E F. 4. Отображение длины объекта LINT По умолчанию установлено отображение двоичной длины объекто LINT. Это можно изменить, вызвав LINT::unsetf (LINT::lintshowlength); LINT::unsetf (ostream, LINT::lintshowlength); для того чтобы длина не отображалась.
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 327 5. Восстановление переменной состояния Переменную состояния для форматирования объекта LINT можно восстановить в предыдущее значение oldflags с помощью вызова двух функций LINT::unsetf (ostream, LINT::flags(ostream)); LINT::setf (ostream, oldflags); Вызовы этих двух функций собраны в перегруженной функции restoref(): LINT::restoref (flag); LINTnrestoref (ostream, flag); Флаги можно объединить, как показано в вызове LINT::setf (LINT::bin | LINT::showbase); Однако это разрешено только лишь для флагов, которые не являются взаимоисключающими. Функции вывода, которые в конечном итоге и генерируют заданный формат объекта LINT, являются расширением оператора ostream& operator <<(ostream& s, LINT In), о котором в общих чертах говорилось ранее. Он оценивает переменную состояния потока вывода и генерирует соответствующий вывод. Для данного оператора применяется вспомогательная функция Iint2str(), содержащаяся в файле flintpp.cpp, которая в свою очередь вызывает функцию xclint2str_l() для того, чтобы представить числовое значение объекта LINT как строку символов: ostream& operator« (ostream& s, const LINT& In) { USHORTbase = 16; long flags = LINT::flags (s); char* formattedjint; if (Iln.init) LINT::panic (E_LINT_VAL, "ostream operator«"f 0, _LINE_); if (flags & LINT::linthex) { base = 16; }
128 Криптография на Си и C++ в действии else { if (flags & LINT::lintdec) { base = 10; } else { if (flags & LINT::lintoct) { base = 8; } else { if (flags & LINT::lintbin) { base = 2; } } } } if (flags & LINT::lintshowbase) { formattedjint = Iint2str (In, base, 1); } else { formattedjint = Iint2str (In, base, 0); } if (flags & LINT::lintuppercase) { struprj (formattedjint); } s «formattedjint«flush;
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 329 if (flags & LINT::lintshowlength) { long Jlags = s.flags (); //Получение текущего состояния s.setf (ios::dec); //установка флага для десятичного отображения s « endl « Id (In) « " bit" « endl; //восстановление предыдущего состояния s.setf (Jlags); } ■>w\ return s; } 14.3.2. Манипуляторы Опираясь на предыдущие механизмы, в данном разделе мы хотим достичь более подходящих вариантов для управления форматом вывода для объектов LINT. Для этого мы используем манипуляторы, которые размещены прямо в потоке вывода, и, их эффект аналогичен тому, что мы получали при вызове ранее рассмотренных . .„,,..... функций состояния. Манипуляторы являются адресами функций, для которых существует специальный вид оператора поместить ("«"), который в свою очередь в качестве аргумента принимает указатель на функцию. Для примера мы рассмотрим следующую функцию: ostream& LintHex (ostream& s) { щ LINT::setf (s, LINT::linthex); return s; } '■'>H<;; fc. Данная функция вызывает функцию состояния setf(s, LINT::linthex) в контексте заданного потока вывода ostream& s и, таким образом, задает формат вывода объектов LINT в форме шестнадцатиричных чисел. Имя функции LintHex без круглых скобок рассматривается как указатель на функцию (см. [Lipp], страница 202), и он может быть направлен в поток вывода как манипулятор с помощью оператора "«" ostream& ostream::operator« (ostream& (*pf)(ostream&)) {
во Криптография на Си и C++ в действи I return (*pf)(*this); I } I определенного в классе ostream: I LINT a ("0x123456789abcdef0"); I cout « LintHex « a; I ostream s; I s « LintDec « a; I Функции-манипуляторы LINT работают по приведенному шаблону I как стандартные манипуляторы в библиотеке языка C++, например I dec, hex, oct, flush, и endl. Оператор "«" просто вызывает мани- I пулятор функции LintHex() или LintDecQ в подходящий момент. I Манипуляторы обеспечивают установку флагов состояния, принад- I лежащих потокам вывода cout и, соответственно, s. Перегруженный I оператор "«" вывода объектов LINT переносит представление I объекта а типа LINT в запрошенную форму. ту Все установки формата вывода объектов LINT могут быть выпол- 1 нены с помощью манипуляторов, представленных в Таблице 14.2. Манипулятор LintBin LintDec LintHex LintOct LintLwr LintUpr LintShowbase LintNobase LintShowlength LintNolength Результат вывода значений LINT как числа в бинарном виде как числа в десятичном виде как числа в шестнадиатиричном виде как числа в восьмеричном виде ё с символами нижнего регистра а, Ь, с, d, е, f 1 для шестнадцатиричного представления * с символами верхнего регистра А, В, С, D, E, F для шестнадцатиричного представления с префиксом для числового представления (Ох или ОХ в шестнадцатиричном и Ob - бинарном) без префикса для числового представления с отображение числа разрядов без отображения числа разрядов В добавление к манипуляторам в таблице 14.2., которым не требуется аргумент, доступны следующие манипуляторы: LINT_omanip<int> SetLintFlags (int flags) Таблица 14.2. Манипуляторы LINT и результат их выполнения
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 331 и LINT_omanip<int> ResetLintFlags (int flags) Они применяются в качестве альтернативы функциям состояния setf() и unsetf(): cout « SetLintFlags (LINT::flag) « ...; // включить cout « ResetLintFlags (LINT::flag) « ...; // выключить Реализацию этих манипуляторов читатель может найти в исходных текстах (flintpp.h и flintpp.cpp), а пояснения по поводу класса- шаблона class omanip<T> в [Pla2], Глава 10. Флаги LINT еще раз показаны в Таблице 14.3. Флаг lintdec lintoct linthex lintshowbase lintuppercase lintbin lintshowlength Значение 0x010 0x020 0x040 0x080 0x100 0x200 0x400 Следующим примером мы внесем ясность в применение самих функций форматирования и манипуляторов: #include "flintpp.h" #include <iostream.h> #include <iomanip.h> main() { LINT n ("0x0123456789abcdef); // число LINT с основанием 16 long deflags = LINT::flags(); // запомним флаги cout « "Представление по умолчанию:" « n « endl; Таблииа 14.3. Флаги LINTлля форма тирования вывола -с сом ^ J I ill ;
I 332 Криптография на Си и C++ в действии I LINT::setf (LINT::linthex | LINT::lintuppercase); I cout « "Шестнадцатиричное представление символов I в верхнем регистре:" « n « endl; f cout « LintLwr « " Шестнадцатиричное представление символов в нижнем регистре:" « n « endl; cout « LintDec « "десятичное представление:" « n « endl; cout « LlntBin « "двоичное представление:" « n « endl; cout « LintNobase « LintHex; cout « "представление без префикса:" « n « endl; cerr« "Представление в потоке cerr по умолчанию:" « n « endl; LINT::restoref (deflags); cout« "представление по умолчанию:" « n « endl; return; } 14.3.3. Файловый ввод/вывод для объектов LINT Для реальных приложений не обойтись без функций вывода объектов LINT в файлы и ввода их из них. Классы ввода/вывода стандартной библиотеки C++ содержат функции-члены, которые позволяют помещать объекты в поток ввода и вывода для файловых операций, поэтому нам повезло в том, что мы можем использовать тот же синтаксис, что мы применяли ранее. Операторы, необходимые для файлового вывода, похожи на те, что были в последнем разделе, однако мы можем обойтись без форматирования. Мы определим два оператора friend ofstream& operator« (ofstream& s, const LINT& In); friend fstream& operator« (fstream& s, const LINT& In); для потоков вывода класса ofstream и для потоков класса fstream, который поддерживает оба направления, то есть как ввод, так и вывод. Так как класс ofstream унаследован от класса ostream, мы можем использовать его функцию-член ostream::whte() для записи неформатированных данных в файл. Поскольку сохранятся только лишь те разряды объекта LINT, которые на самом деле используются, мы будем экономно расходовать место на носителе для запис
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 333 данных. Разряды типа USHORT объекта LINT в действительности будут записаны как последовательность значений типа UCHAR. Для полной уверенности, что это происходит всегда в правильном порядке, независимо от схемы представления чисел на конкретной платформе, определим вспомогательную функцию, которая записывает значения USHORT в виде последовательности двух символов типа UCHAR. Функция устраняет эффект платформозависимого расположения разрядов по основанию 256 в памяти, таким образом позволяя данным, которые были записаны на одном типе компьютеров, быть считанными на других, где, возможно, иначе располагаются байты в слове или иначе интерпретируются данные при их чтении с устройств чтения/записи. В качестве примера для данного случая можно привести архитектуры с прямым (little-endian) и обратным (big-endian) порядком байт, в первой из которых последовательное приращение адресов памяти выполняется в порядке их возрастания, а во втором - в порядке убывания2" template <class T> int wnte_ind_ushort (Т& s, dint src) { UCHAR buff[sizeof(clint)]; unsigned i, j; for (i = 0, j = 0; i < sizeof(clint); i++, j = i « 3) { buff[i] = (UCHAR)((src & (Oxff«j)) »j); } s.write (buff, sizeof(clint)); if(!s) { return-1; } else { return 0; 2 Два байта В, и B/+i с адресами / и /+1 интерпретируются в представлении little-endian как значение USHORT w = 28В;И + В,, а в представлении big-endian как w=28B,+Bl+1. Аналогичная ситуация рассматривается для толкования значений ULONG.
334 Криптография на Си и C++ в действии } } Функция write_ind_ushort() в случае ошибки возвращает значение -1, а когда выполнение операции является успешным, возвращается 0. Она реализована как template (шаблон) для того, чтобы его можно было использовать двумя объектами, как ofstream, так и fstream. Функция readJnd_ushort() создана по аналогу предыдущей функции и обратна ей: template <class T> jnt readJnd_ushort (T& s, dint *dest) { UCHAR buff[sizeof(clint)]; unsigned i; s.read (buff, sizeof(clint)); if (Is) { return-1; } else { *dest = 0; for (i = 0; i < sizeof(clint); i++) { *dest |= ((clint)buff[i]) « (i « 3); } return 0; } } Теперь операторы вывода используют этот промежуточный формат для записи объекта LINT в файл. Чтобы прояснить ситуацию, мы предоставим реализацию оператора для класса ofstream.
ГЛАВА Л4. Открытый интерфейс LINT: члены и друзья класса 335 ofstream& operator« (ofstream& s, const LINT& In) { jf (Nn.init) { LINT::panic (EJ_INT_VAL, "ofstream operator«", 0, _LINE_); } oi for (int i = 0; j <= DIGITS_L (In.nJ); j++) { if (writeJnd_ushort (s, ln.n_l[i])) { LINT::panic (EJJNT_EOF, "ofstream operator«", 0, _LINE_); } } return s; } Прежде чем объект LINT будет записан в файл, файл должен быть открыт на запись, для которого может быть использован конструктор ofstream:-.ofstream (const char *, openmode) или функция-член ofstream::open (const char *, openmode) В каждом случае должен быть установлен флаг ios::binary, как показано в следующем примере: .тц Л:Н LINT r ("0x0123456789abcdef"); II... ofstream fout ("test.io", ios::out | ios::binary); fout « r« r*r; II... fout.closeQ;
336 Криптография на Си и C++ в действии Импорт объекта LINT из файла осуществляется в обратном порядке, с операторами, аналогичными выводу объекта LINT в файл. friend jfstream& operator» (ifstream& s, LINT& In); friend fstream& operator» (fstream& s, LINT& In); Оба оператора сперва считывают отдельное значение, которое определяет число разрядов в сохраненном объекте LINT. Далее считывается соответствующее число разрядов. Значения USHORT считываются согласно описанию выше с помощью функции read_ind_ushort(): ifstream& operator» (ifstream& s, LINT& In) { if (read_ind_ushort (s, In.nJ)) { LINT::panic (E_LINT_EOF, "ifstream operator»", 0, _LINE_); } if (DIGITSJL (In.nJ) < CLINTMAXSHORT) { for (int i = 1; i <= DIGITS J_ (In.nJ); i++) if (readJnd_ushort (s, &ln.nj[i])) { LINT::panic (E_LINT_EOF, "ifstream operator»", 0, _LINE_); } } } // Никакой паранойи! Проверка импортируемого значения if (vcheckj (In.nJ) == 0) { :i:- In.init = 1; } else
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 337 In.init = 0; } return s; } Для того чтобы открыть файл, из которого будет считан объект LINT, снова необходимо установить флаг ios::binary: LINT r, s; ifstream fin; fin.open ("test.jo", ios::in | ios::binary); fin » r» s; II... fin.close(); В процессе импорта объектов LINT оператор "»" проверяет, представляют ли считанные значения числовое представление правильного объекта LINT. Если это не так, то величина init устанавливается в 0, и, таким образом, целевой объект отмечен как "неинициализированный". Следующую операцию над таким объектом LINT выполнит обработчик ошибок, который мы рассмотрим более детально в следующей главе.
ГЛАВА 15. Обработка ошибок О отвратительная ошибка, дитя меланхолии! Шекспир, Юлий Цезарь 15.1. (Без) Паники ... Функции C++, представленные в предыдущих главах, реализовы- вают механизмы анализа того, произошла ли в процессе выполнения вызванной С-функции ошибка или иная ситуация, которая требует подробного ответа, или, по крайне мере, предупреждения. Функции проверяют, были ли инициализированы переданные им переменные и оценивают возвращаемое значение вызванных функций С: LINT f (LINT argl, LINT arg2) { LINT result; Int err; if (largl.init) LINT::panic (E_LINT_VAL, T, 1, _LINE_); if (!arg2.init) LINT::panic (E_LINT_VAL, T, 2, _LINE_); // Вызвать С-функцию для выполнения операции, код ошибки вернется в err • err = f_l (argl .n_l, arg2.n_l, result.nj); ■yJO' щъ switch (err) { case 0: ms" result.init = 1; break; case E_CLINT__OFL: result.status = E_LINT_OFL;
340 Криптография на Си и C++ в действии result.init = 1; break; case E_CLINT_UFL: result.status = EJJNTJJFL; * <- result.init = 1; :».0\,i««.^! break; default: LINT::panic (EJ_INT_ERR, T, err, _LINE_); } |f return result; } \u Если переменная status содержит значение EJJNT_OK, то это явля- >-ш-l;- <- ется наилучшем случаем. В более затруднительных ситуациях, в которых происходит переполнение или потеря значимости в функции С, переменная status принимает соответствующие значения EJJNTJ3FL или EJJNTJJFL Так как наши функции С реагируют на потерю данных приведением по модулю 7Vmax + 1 (см. стр. 32), в этом случае функции завершаются нормально. Значение переменной status можно будет запросить функцией-членом LINT_ERRORS LINT::Get_Warning_Status (void); Кроме этого, мы видели, что функции LINT всегда вызывают функцию с хорошо подобранным именем panicQ, когда ситуация накаляется так, что уже невозможно ее обработать внутри функции. Задача этой функции-члена сперва вывести сообщения об ошибках, чтобы пользователь программы осознал, что что-то проходит неверно, и, во-вторых, убедиться в управляемом завершении программы. Сообщения об ошибках LINT выводятся через поток сегг, они содержат информацию о природе произошедшей ошибки, о функции, обнаружившей ошибку, и об аргументах, ее инициировавших. Чтобы вывести всю эту информацию, ее необходимо полу чить от вызываемой функции, как показано в следующем примере: LINT::panic (EJJNTJDBZ,"%", 2, LINE ); В данном выражении объявляется появление деления на ноль операторе "%" в строке, заданной ANSI макросом LINE , кото рое вызвано вторым аргументом этого оператора. Аргументы нуме руются так: 0 - всегда обозначает неявный аргумент функции- члена, а все другие аргументы пронумерованы слева направо,
ГЛАВА 15. Обработка ошибок 341 начиная с 1. Функция обработки ошибок LINT panic() выводит сообщения об ошибках следующих типов: Пример 1: Применение неинициализированного объекта LINT в качестве аргумента. Critical run-time error detected by class LINT: Argument 0 in Operator *= uninitialized, line 1997 ABNORMAL TERMINATION Пример 2: Деление объекта LINT на значение О Critical run-time error detected by class LINT: Division by zero, operator/function/, line 2000 ABNORMAL TERMINATION Функция и операторы класса LINT распознают ситуацию, рассмотренную в Таблице 15.1. Таблица 15.1, Колы ошибки функции LINT Код E_LINT_OK E_LINT_EOF E_LINT_DBZ E_UNT_NHP E_UNT_OFL E_LINT_UFL E_LINT_VAL E_LINT_BOR E_LlNT_MOD E_LINT_NPT Значение 0x0000 0x0010 0x0020 0x0040 0x0080 0x0100 0x0200 0x0400 0x0800 0x1000 Пояснение Все в порядке Ошибка файлового ввода/вывода в поточном операторе « или » Деление на ноль Ошибка выделения памяти: new вернул указатель NULL Переполнение в функции или операторе Потеря значимости в функции или операторе Аргумент функции не инициализирован или имеет недопустимое значение Неверное основание, переданное конструктору в качестве аргумента Четный модуль в mexpkmQ Указатель NULL, переданный в качестве аргумента.
342 Криптография на Си и C++ в действии, 15.2. Обработка ошибок, определяемая пользователем Как правило, бывает необходимо приспособить обработку ошибо к определенным требованиям. Класс LINT предоставляет такие возможности, позволяя пользователю заменить LINT-функцию ошибки panic() на его собственные функции. В добавление к этому, вызывается следующая функция, которая берет в качестве аргумента указатель на функцию: void LINT::Set_LINT_Error_Handler (void (*Error_Handler) (LINT_ERRORS, const char* const, const int, const int)) { LINTJJser_Error_Handler = ErrorJHandler; } Переменная LINT_User_Error_Handler определена и инициализирована в файле flintpp.cpp как static void (*LINT_User_Error_Handler) (LINT_ERRORS, const char*, const int, const int) = NULL; Если данный указатель имеет значение, отличное от NULL, то установленная функция будет вызвана вместо panic() и ей будет доступна та же информацию, что и panicQ. Что касается реализации пользовательской программы обработки ошибок, то тут фантазии нет предела. Однако необходимо понимать, что ошибки, переданные классом LINT, обычно отражают ошибки программы, которые невозможно исправить в процессе выполнения. Совершенно не имеет смысла возвращать управление в тот программный сегмент, в котором имеют место подобные ошибки, поэтому, вообще говоря, в подобных случаях единственным подходящим выходом из ситуации является завершение программы. Возврат к программе обработки ошибок LINT panic() осуществляется с помощью вызова LINT::Set__LINT_Error_Handler(NULL); Следующий пример демонстрирует интеграцию пользовательской функции в обработку ошибок: #include "flintpp.h" void my_error_handler (LINT_ERRORS err, const char* str, const int arg, const int line)
ГЛАВА 15. Обработка ошибок 343 { //... Код } main() { // активация пользовательского обработчика ошибок: LINT::Set_LINT_Error_Handler (my_error_handler); //... Код // восстановление обработчика ошибок LINT: LINT::Set_LINT_Error_Handler(NULL); //...Код } 15.3. Исключения LINT Механизм исключений языка C++ является инструментом, который ч*г прост в применении, а следовательно, и более эффективен в обработке ошибок, нежели методы, предлагаемые С. Обработчик ошибок LINT::panic(), который описан ранее, ограничен выводом сообщений об ошибках и контролируемым завершением программы. Вообще говоря, нам важна информация не о функции деления, где произошло деление на ноль, а о функции, которая вызвала деление и тем самым спровоцировала ошибку. А такая информация в LINT::panic() не передается. В частности, с LINT::panic() невозможно дм-, будет вернуться к этой функции для того, чтобы устранить ошибку ■'шкм*:- или отреагировать иным, характерным для данной функции обра- ji) >эу, зом. С другой стороны, такие возможности предлагает механизм :>:, исключений языка C++, и мы хотели бы здесь создать условия, которые позволят сделать данный механизм пригодным для класса LINT. тэ Исключения в C++ принципиально основываются на конструкциях трех типов: блок try (попытка), блок catch (захват), и команда throw (выброс), с помощью которой функция подает сигнал об ошибке. У блока catch имеется функция локальной обработки ошибок блока try: блоком catch, который следует за блоком try, будут выявлены ошибки, находящиеся в try и объявлены с помощью throw.
344 Криптография на Си и C++ в действии Дальнейшие команды блока try будут проигнорированы. Тип ошибки будет отражен в значении команды throw в качестве параметра сопроводительного выражения. Связь между блоками try и catch может быть отражена следующим образом: try { • • • • // Если ошибка вызвана через оператор throw, • • • • II ю ее можно поймать • • • • // следующим блоком catch } catch (argument) { • • • • // здесь находятся процедуры обработки ошибок. } Если ошибка произошла не прямо в блоке try, а в функции, котор вызывалась из него, то данная функция завершает свое выполнение и управление возвращается вызываемой функцией по цепочке вызовов в обратном порядке, пока не будет достигнут блок try. Начиная с этого момента, управление передается соответствующему блоку catch. Если блок try не найден, то вызывается генерация программы обработки ошибок, добавленной компилятором, которая в дальнейшем завершает программу, зачастую с каким-нибудь стандартным выводом. Вполне ясно, какие ошибки могут быть в классе LINT, поэтому совершенно не составит труда вызвать throw с теми кодами ошибки, которые были предоставлены методу panic() функциями и операторами LINT. Однако следующее решение являются более удобным: сперва мы определяем абстрактный базовый класс class LINTJError { public: char* function; int argno, lineno;
ГЛАВА 15. Обработка ошибок 345 virtual void debug__print (void) const = 0; // чистая виртуальная virtual ~LINT_Error() {}; }; точно так же, как классы следующего типа, основанного на нем: // деление на ноль class LINTJDivByZero : public LINTJError { public: LINT_DivByZero (const char* const tunc, const int line); void debug_print (void) const; }; LINT_DivByZero::LINT_DivByZero (const char* const tunc, const int line) { function = func; lineno = line; argno = 0; } void LINT_DivByZero::debug_print (void) const { cerr« "LINT-Исключение:" « endl; cerr« "деление на ноль в функции " «function « endl; cerr« "в модуле:" « FILE « ", в строке:" «lineno « endl; } Для любого типа ошибок существует класс, который, как показано в этом примере, может быть использован с throw LINTJ3ivByZero(function, line); для уведомления о конкретной ошибке. В целом, следующие подклассы базового класса LINTJError определены как: >QN/- -атэб ' «чУч
346 Криптография на Си и C++ в действии class LINT_Base : public LINT_Error // неправильное основание ..}; class LINT_DivByZero : public LINTJError //деление на ноль class LINT_Emod : public LINT_Error // четный модуль для mexpkm class LINT_File : public LINT_Error {...}; // ошибка при файловом вводе/выводе class LINT_Heap : public LINT_Error // ошибка выделения памяти при new class LINTJnit: public LINT_Error // аргумент функции // непредусмотрен // или не инициализирован class LINT_Nullptr: public LINT_Error // пустой указатель, // переданный в качестве // аргумента class L1NT.OFL : public LlNT_Error // переполнение в функции class LINT__UFL : public LINTJError {...}; // потеря значимости в функции С одной стороны, так мы можем отловить ошибки LINT, не различ специально, какая ошибка произошла, вставив блок catch
ГЛАВА 15. Обработка ошибок 347 catch (LINT_Error const &err) // Примечание: LINT__Error - { // абстрактный класс II... err.debug_print(); //... } после блока try, с другой стороны, мы сможем сконцентрироваться на поиске определенной ошибки, если зададим соответствующий аргумент - класс ошибки - в операторе catch. Необходимо отметить, что как абстрактный класс LINT_Error не тиражируется как объекты, поэтому его аргумент err может быть передан только с помощью ссылки, а не значения. Несмотря на то, что все функции LINT содержат обработчик ошибок panic(), применение исключений вовсе не означает то, что мы должны изменять все функции. Мы, скорее, объединим соответствующие операции throw в процедуру panic(), где они вызываются в соответствии с той ошибкой, о которой сообщается функции. Потом управление передается блоку catch, который принадлежит блоку try вызываемой функции. Следующий фрагмент кода функции panic() проясняет образ действия: void LINT::panic (LINT_ERRORS error, const char const * func, const int arg, const int line) { if (LINTJJser_Error_Handler) { LINTJJser_ErrorJHandler (error, func, arg, line); } else { cerr« "Критическая ошибка во время исполнения, обнаруженная классом LINTAn"; switch (error) { case EJJNTJDBZ:
348 Криптография на Си и C++ в действии сегг « "деление на ноль, оператор/функция " «tunc « ", в строке " « line « endl; #jfdef LINT.EX throw LINT_DivByZero (tunc, line); #endif break; //... } } } Поведение результатов в случае ошибки может полностью контролироваться пользовательскими подпрограммами для обработки ошибок без необходимости вмешательства в реализацию LINT. Кроме того, обработку исключений можно полностью отключить, что является необходимым, когда этот механизм не поддерживается используемым компилятором C++. В случае описанной функции panic() исключения должны быть явно включены с помощью определения макроса LINT_EX, например, опцией компилятора -DLINTJEX. Некоторые компиляторы требуют указания дополнительных опций для того, чтобы активизировать обработку исключений. Для более близкого рассмотрения рассмотрим небольшой пример, демонстрирующий исключения LINT: #include "flintpp.h" main(void) { LINTa = 1,b = 0; try { b = a / b; // ошибка: деление на ноль } catch (LINTJDivByZero error) // обработка ошибки { // деления на ноль
ГЛАВА 15. Обработка ошибок 349 error.debug_print (); cerr« "деление на ноль в модуле" « FILE « ", в строке" « LINE ; } } Оттранслированная с помощью вызова GNU gcc дсс -fhandle-exceptions -DLINT_EX divex.cpp flintpp.cpp flint.c -lstdc++ программа, в добавление к сообщениям об ошибках функции panic(), выдаст следующий вывод: LINT-Exceptlon: деление на ноль, оператор/функция / в модуле: flintpp.cpp, в строке: 402 деление на ноль в модуле divex.cpp, в строке 17 Существенным отличием между этой и стандартной обработкой ошибок без исключений является то, что мы выясняем с помощью процедуры catch, где действительно произошла ошибка, а именно в строке 17 модуля divex.cpp, хотя она проявилась в другом месте - в модуле flintpp.cpp. Для отладки больших программ это является совершенно необходимым источником информации.
ГЛАВА 16. Практический пример: криптосистема RSA Напрашивался очевидный вопрос: Можно ли это сделать с помощью обычного шифрования? Можем ли мы создать защищенное зашифрованное сообщение, которое смог бы прочитать '"' авторизованный получатель без какого-либо *'г предварительного обмена секретными ключами °*-* и т.д.?» ... Я опубликовал теорему существования в 1970 году. Дж. X. Эллис. История несекретного шифрования, 1987 Наш рассказ близится к концу, и теперь нам хотелось бы проверить то, что мы делали глава за главой, на живом современном примере, ;< который бы показал нам связь разработанных нами многочисленных функций с криптографией. После краткого экскурса в область !-м[ асимметричных криптосистем мы обратимся к классическому примеру такой системы - алгоритму RSA, который был изобретен и опубликован в 1978 году Рональдом Ривестом, Ади Шамиром и Леонардом Адельманом (см. [Rive], [Elli]) и который теперь используется во всем мире. 1 Алгоритм RSA запатентован в Соединенных Штатах Америки, однако срок патента истек 20 сентября 2000 года. Против свободного использования алгоритма RSA выступала компания RSA Security, обладавшая правами на фирменное 1 ( название «RSA», что вызвало неистовую дискуссию при работе над стандартом Р1363 [ШЕЕ]. Появлялись, порой, абсурдные предложе- ' ' ния: переименовать процедуру RSA в «бипростую криптографию» (biprime) или, менее радикально, FRA (former RSA algorithm - бывший алгоритм RSA), RAL (по первым буквам имен авторов алгоритма - Ron, Adi, Leonard) и QRZ (RSA - 1 - каждая буква названия заменяется на предыдущую по модулю 26). По истечении срока патента компания RSA Security вынесла следующий вердикт: Ясно, что словосочетания «алгоритм RSA», «алгоритм с открытым 0|j ключом RSA», «криптосистема RSA» и «криптосистема с откры- • I/*- тым ключом RSA» прочно обосновались в стандартах и открытой i учебной литературе. Компания RSA Security не собирается запрещать использование этих терминов частными лицами или организациями, реализующими алгоритм RSA («RSA-Security - Behind the Patent», сентябрь 2000).2 По данным http://www.rsasecurityxom, к 1999 году было продано более трехсот миллионов продуктов, использующих алгоритм RSA. http://www.rsasecurity.com/developers/total-solution/faq.htm (9/2000)
352 Криптография на Си и C++ в действии 16.1. Асимметричные криптосистемы Идея, положенная в основу асимметричных криптосистем, была опубликована Уитфилдом Диффи (Whitfield Diffie) и Мартином Хеллманом (Martin Hellman) в революционной статье «Новые направления в криптографии» (см. [Diff]). Асимметричные криптосистемы, в отличие от симметричных алгоритмов, используют для зашифрования и расшифрования сообщений не один секретный ключ, а пару ключей для каждого участника протокола - открытый ключ Е для зашифрования и не совпадающий с ним секретный ключ D для расшифрования. Для этих ключей, последовательно примененных к сообщению М, должно выполняться соотношение (16.1) D{E(M)) = M. Это соотношение можно рассматривать как замок, который можно запереть одним ключом, а открыть другим. Для безопасности такой системы секретный ключ D должен быть таким, чтобы его нельзя было вычислить по открытому ключу Е или чтобы указанное вычисление было неосуществимо из-за ограничений времени и памяти. В отличие от симметричных систем в асимметричных работать с ключами несколько проще. Чтобы получатель А, владелец секретного ключа, смог расшифровать сообщение, зашифрованное отправителем В, по каналам связи нужно передать только открытый ключ участника А.Этот принцип и определяет открытость сеанса связи: для безопасного взаимодействия двух участников достаточно договориться об асимметричной процедуре шифрования и обменяться открытыми ключами. Не нужно обмениваться никакой секретной информацией. Однако прежде чем наша эйфория выйдет из-под контроля, заметим все же, что в общем случае даже в асимметричных криптосистемах не удается избежать некоторого управления ключами. Как участники предположительно безопасного сеанса связи, мы должны быть уверены, что открытые ключи других участников подлинны, то есть, чтобы нарушитель, влекомый низменной идеей перехвата секретной информации, не смог незаметно внедриться в сеанс связи и выдать свой ключ за открытый, будто бы принадлежащий проверенному партнеру. Подлинность открытых ключей гарантируется на удивление сложными процедурами, и уже существуют законы на бумаге, регулирующие деятельность в этой области. Ниже мы рассмотрим этот вопрос более подробно. Принцип асимметричных криптосистем распространяется гораздо дальше - он позволяет создавать цифровые подписи, в которых ключевая функция переворачивается задом наперед. Для формирования цифровой подписи мы «зашифровываем» сообщение на секретном ключе и передаем то, что получилось, вместе с сообщением
ГЛАВА 16. Практический пример: криптосистема RSA 353 по каналу связи. Теперь любой, кто знает соответствующий открытый ключ, может «расшифровать» «зашифрованное» сообщение и сравнить результат с исходным текстом. Напротив, сформировать ч цифровую подпись может только обладатель секретного ключа. «ой Отметим, что по отношению к цифровым подписям использование терминов «зашифрование» и «расшифрование» не совсем корректно, поэтому будем говорить о «формировании» и «проверке» цифровой подписи. с К асимметричным системам, формирующим цифровые подписи, предъявляется следующее жесткое требование: зависимость значе- о: ния D(M) от сообщения М должна быть проверяемой. Такая про- v верка возможна, если математические операции зашифрования и расшифрования коммутативны, то есть результатом поочередного их выполнения (неважно в каком порядке) должно быть одно и то ПТ' же, а именно исходное, сообщение М: (16.2) D{E(M)) = E(D(M)) = М. ri Применяя открытый ключ Е к значению D{M), можно проверить, является ли D(M) правильной цифровой подписью для сообщения М Развитие цифровых подписей чрезвычайно важно сегодня в двух аспектах: Q • Законы о цифровой (или электронной) подписи в Европе и США создают предпосылки к дальнейшему использованию цифровых подписей в легальных транзакциях. • Растущая роль Интернета в электронной коммерции стимулирует применение цифровых подписей для идентификации и аутенти- ,';'. фикации участников коммерческой транзакции, аутентификации цифровой информации и обеспечения безопасности финансовых транзакций. * ' Интересно отметить, что использование терминов «электронная * подпись» и «цифровая подпись» отражает два разных подхода к законам о подписи. Для электронной подписи все идентифицирую- f , щие средства: электронные символы, буквы и рисунки используются для аутентификации документа. Цифровая подпись, напротив, явля- г ется результатом электронной процедуры аутентификации, основанной на процессах информационных технологий, то есть призва- на проверять целостность и подлинность передаваемого текста. Эти термины часто путают, смешивая, таким образом, два совершенно разных технических процесса (см., например, [Mied]).3 Хотя законы об электронной подписи, как правило, оставляют открытым вопрос об алгоритме цифровой подписи, большинство про- В России к этому подошли просто - в стандартах фигурирует электронная цифровая (!) подпись. — Прим. перев.
354 Криптография на Си и C++ в действии токолов идентификации, аутентификации и авторизации, как проектируемых, так и уже применяемых для электронных транзакций через Интернет, используют алгоритм RSA, который, вероятно, и дальше никому не уступит в этой области. Таким образом, цифровая подпись RSA как нельзя лучше подходит для реализации функций FLINT/C. Автор отдает себе отчет в том, что эта глава служит лишь кратким введением в чрезвычайно важный раздел криптографии. Однако такая краткость оправдана наличием большого числа исчерпывающих публикаций на эту тему. Читателю можно порекомендовать [Beut], [Fumy], [Salo] и [Stin] в качестве вводных источников, более разносторонние работы [MOV] и [Schn], а также математические монографии [Kobl], [Kran] и [HKW]. 16.2. Алгоритм RSA Ложно почти все, что не более чем правдоподобно. Рене Декарт Теперь кратко опишем математические свойства алгоритма RSA и посмотрим, как на его основе можно реализовать шифрование с открытым ключом и цифровую подпись. Придерживаясь математических свойств алгоритма RSA, разработаем классы C++ для каждой из функций зашифрования сообщения, расшифрования сообщения, формирования подписи и проверки подписи. А для начала поясним пути реализации этих функций в нашем классе LINT. Самым важным элементом алгоритма RSA является пара ключей, имеющих конкретный математический смысл. Пара ключей RSA формируется с помощью трех основных элементов: модуля п, компонента е открытого ключа (для зашифрования) и компонента d секретного ключа (для расшифрования). Открытым и секретным ключом являются пары (е, п) и (d, n) соответственно. Сначала сгенерируем модуль п как произведение п = pq двух простых чисел р и q. Если ф(/г) = (р- l)(q - 1) - функция Эйлера (см. стр. 198), то для данного модуля п компоненту е открытого ключа можно выбрать из условий е < ф(и) и НОД(е, ф(л)) = 1 • Декретная компонента d вычисляется как d = e~l mod ф(я) - число, мультипликативно обратное к е по модулю п (см. п. 10.2). Проиллюстрируем этот принцип на небольшом примере. Выберем Р = 7 и q=\\. Тогда п = 77 и ф(л) = 22 • 3 • 5 = 60. Из условия НОД(е, ф(/г)) = 1 заключаем, что наименьшее число, которое можно выбрать в качестве е, это 7, откуда получаем значение d = 43, поскольку 7 • 43 = 301 = 1 mod 60. С этими значениями можем применить алгоритм RSA к игрушечному примеру: зашифровав «сообщение» 5,
ГЛАВА 16. Практический пример: криптосистема RSA 355 получаем 57 mod 77 = 47; расшифрованием 4743 mod 77 = 5 восстанавливаем исходное сообщение. Вооружившись такими ключами (вскоре мы обсудим, какой должна быть реальная длина различных компонент ключа) и подходящим программным обеспечением, мы теперь можем безопасно обмениваться информацией друг с другом. Чтобы проиллюстрировать процедуру RSA, рассмотрим процесс передачи сообщения, зашифрованного алгоритмом RSA, отправителем А получателю В: 1. Участник В генерирует свой ключ RSA с компонентами лв, с1в и ев, а затем сообщает открытый ключ {ев, ив) участнику А. 2. Пусть теперь участник А хочет отправить зашифрованное сообщение М (0<М<пв) участнику В. Получив от В открытый ключ, А вычисляет С = л/Впкх1лв , i'iT ■'■ и посылает зашифрованное сообщение С участнику В. ■ j 3. Участник В, получив от А зашифрованное сообщение С, расшиф- ;^v ровывает это сообщение, вычисляя М-С mod ив с помощью своего секретного ключа (de, ив). Теперь у В есть текст исходного сообщения М. Нетрудно видеть, почему это все работает. Так как d • е = mod ф(п), существует целое число к такое, что d • е = 1 + к • ф(л). Тогда (16.3) С1 s Afle ее М1+к'т ее М - (М*п))к ее и mod л, при этом мы использовали теорему Эйлера, приведенную на J -; стр. 198, согласно которой М^п) ее l mod л, если НОД(М, л) = 1. 4iv Ясно, что безопасность криптосистемы RSA зависит от того, на- '* сколько легко разложить число л на множители. Как только л раз- 10 ложено в произведение чисел р к q, секретный ключ d сразу может п •'■ быть вычислен по секретному ключу е. Обратно, легко найти раз- '*- * ложение числа л, если известны обе компоненты d и е. Действительно, если положить k:=de-l, то число к будет кратным значе- n,v нию ф(л) и, значит, к = г • 2', где число г нечетное и t > 1. Для любого g е Z„ справедливо g* = g'/e-1 ее gg~x = l mod л, то есть g*/2 - квадратный корень из 1 по модулю л. Таких корней всего четыре: ±1 и ±х9 где х= 1 modр и jc = —1 modg. Таким образом, /? | (х- 1) и # | (х + 1) (см. п. 10.4.3). Вычисляя р = НОД(д:- 1, л), получаем делитель числа л (см. стр. 235). Сложность других атак на криптосистему RSA либо совпадает со сложностью разложения, либо использует слабости конкретных протоколов, но не самого алгоритма RSA. Современное состояние
356 Криптография на Си и C++ в действии дел показывает, что атаки на алгоритм RSA возможны при следующих условиях: 1. Общий модуль Использование общего модуля для нескольких пользователей дае очевидную уязвимость: учитывая то, что мы только что сказали, каждый участник может с помощью своих компонент ключа е и d разложить общий модуль n=pq. Зная множители р и q, а также компоненты открытых ключей других пользователей, он может вычислить и их секретные ключи. 2. Малый открытый показатель Поскольку время вычислений в алгоритме RSA для данного модуля п целиком зависит от размера показателей end, очень хочется выбрать эти показатели как можно меньше. Например, при показателе, равном 3 (наименьший возможный показатель), требуется всего одно возведение в квадрат и одно умножение по модулю п - так почему бы не сэкономить время вычислений? -- • Допустим, нарушитель смог перехватить три зашифрованных сообщения Сь С2 и С3, каждое из которых является шифрограммой одного и того же открытого теста М, зашифрованного на трех разных ключах (3, ni) тремя разными отправителями: С\ = М3 mod ti\, С2 = М3 mod пъ С3 = Мъ mod /i3. Вполне вероятно, что НО ДО*,, nj) = 1 при [Ф]. Тогда нарушитель может воспользоваться китайской теоремой об остатках (см. стр. 225) и найти значение С, для которого С = М3 mod n\п2пъ. Поскольку верно и то, что М3 < п\п2пъ, отсюда получаем, что значение С в точности равно М3 и нарушитель может вычислить М просто как корень . Такого рода широковещательные атаки {broadcast attacks) всегда можно реализовать, если число шифрограмм С/ больше, чем открытый показатель. Кроме того, открытые тексты даже не обязательно должны совпадать, а могут быть линейно зависимы, то есть связаны соотношениями вида Мх = а + Ь • Ы\ (см. [Bone]). Таким образом, чтобы предотвратить такую атаку, не нужно выбирать открытые показатели слишком маленькими (в любом случае, они должны быть не меньше, чем 216+ 1 =65537) и, кроме того, прежде чем зашифровывать широковещательные сообщения, в них следует внести случайную избыточность. Для этого можно, например, «растянуть» сообщение до некоторого подходящего числового значения, не превышающего модуль. Такой процесс называется дополнением (см. стр. 370 и [Schn], п. 9.1).
ГЛАВА 16. Практический пример: криптосистема RSA 357 3. Малый секретный показатель Еще хуже, если мал секретный показатель. Еще в 1990 году М. Винер (М. Wiener) [Wien] показал, как, зная открытый ключ (е, п), где 1 < е < ф(н), можно вычислить компоненту d секретного ключа, если число d слишком мало. Результат Винера был недавно усилен Д. Бонехом (D. Boneh) и Г. Дурфи (G. Durfee) [Bone], которые показали, что d можно вычислить из (е, п), если d<n0,292. Существует гипотеза, что это же верно и для d<n0'5. Таким образом, для типичных размеров модуля RSA получаем следующие ограничения на секретный показатель d: п 2768 21024 22048 24096 d 2384 ^512 21024 22048 4. Уязвимости программной реализации Помимо уязвимостей, обусловленных выбором параметров, существует еще множество проблем, вызванных некорректной реализацией, что также может сказаться на безопасности криптосистемы RSA, да и любой другой криптографической процедуры. Нужно с крайней осторожностью использовать чисто программную реали- ^ зацию, не защищенную от внешних атак какими-нибудь аппаратными средствами. Чтение содержимого памяти, наблюдение за поведением шины или состояниями процессора может привести к раскрытию информации о секретном ключе. Минимум что нужно делать - это сразу же после использования очищать оперативную память от всех данных, так или иначе связанных с секретными компонентами RSA (и любой другой криптосистемы). Это можно сделать путем активной перезаписи (например, с помощью функции purgeJ(), которая появлялась на стр. 184). В функциях пакета FLINT/C уже предприняты все необходимые меры. В безопасном режиме локальные переменные и выделенная память перед завершением функции перезаписываются нулями и, таким образом, очищаются. Здесь надо быть очень осторожным, поскольку у компиляторов бывают настолько большие возможности оптимизации, что простую команду, которая, по его мнению, не влияет на завершение функции, он может и проигнорировать. И вот еще: следует помнить, что вызовы функции memset() в стандартной библиотеке языка С игнорируются, если компилятор «не понимает», зачем ее нужно вызывать.
358 Криптография на Си и Of+ в действии Проиллюстрируем все это на примере. В функции f_l() используются две динамические переменные: key_l типа CLINT и secret типа USHORT. Перед завершением функции, содержание которой нас больше не интересует, следует перезаписать память, присвоив О переменной secret и, соответственно, вызвав функцию memset() для переменной кеу_1. Вот соответствующая программа на С: int f_l (CLINT n_l) { • CLINT keyj; USHORT secret; /* Переписываем переменные */ secret = 0; memset (keyj, 0, sizeof (keyj)); return 0; } И что же с этим делает компилятор (Microsoft Visual C/C++ 6.0, компиляция с ключами cl -с -FAs -02)? PUBLIC J ; COMDAT J „TEXT SEGMENT _key_l$ = -516 _secret$ = -520 J PROCNEAR ; COMDAT ;5 : CLINT keyj; ;6 : USHORT secret; ; 18 : /* Переписываем переменные */ ; 19 : secret = 0;
ГЛАВА 16. Практический пример: криптосистема RSA 359 20 21 22 memset (keyj, 0, sizeof (keyj)); return 0; xor eax, eax ;23 :} add esp, 532 ;00000214H ret 0 J ENDP „TEXT ENDS Согласно созданной компилятором программе на Ассемблере, команды удаления переменных keyj и secret не имеют никаких последствий. С точки зрения оптимизации это, конечно, хороший результат. Даже встроенная (inline) версия функции memset() при оптимизации просто удаляется. Однако для приложений, обеспечивающих безопасность, такая стратегия слишком умна. Значит, динамическое удаление переменных, определяющих безопасность, путем перезаписи нужно реализовать так, чтобы оно действительно выполнялось. Отметим, что утверждения (см. стр. 173) могут «подавить» проверку производительности, поскольку при их наличии компилятор вынужден исполнять код программы. Как только утверждения отключены, оптимизация возобновляется. Следующая функция, реализованная в пакете FLINT/C, использует переменное число аргументов и обрабатывает их, в зависимости от размера, как стандартные целые типы, полагая их равными 0. Для других структур данных вызывается функция memset() и выполняется перезапись: static void purgevarsj (int noofvars,...) { vajistap; size_t size; va_start (ap, noofvars); for (; noofvars > 0; -noofvars) { switch (size = va_arg (ap, size_t))
360 Криптография на Си и C++ в действии { case 1: *va_arg (ар, char *) = 0; j break; case 2: *va_arg (ap, short *) = 0; break; case 4: *va_arg (ap, long *) = 0; break; default: assert (size >= CLINTMAXBYTE); I - W ' I memset (va_arg(ap, char *), 0, size); } } va_end (ap); L Функция рассматривает в качестве аргументов пары чисел (длина I переменной в байтах, указатель на переменную); в noofvars указы- I вается также число таких пар. I Обобщением этой функции является макрос PURGEVARS_L(): I #ifdef FLINT_SECURE I #define PURGEVARS_L(X) purgevarsj X 1 #else I #define PURGEVARS_L(X) (void)0 1 #endif /* FLINT_SECURE 7 I позволяющий при необходимости включать и отключать безопас- I ный режим. Удаление переменных в функции f() выполняется так: I /* Переписываем переменные */ I PURGEVARS_L ((2, sizeof (secret), &secret, sizeof (keyj), keyj)); I Компилятор не может проигнорировать вызов этой функции исходя I из обычных принципов оптимизации, такое могло бы случится I только при проведении чрезвычайно мощной глобальной оптими- I зации. В любом случае, действенность таких средств защиты можно I проверить, просматривая код программы: f PUBLIC J EXTRN _purgevars_l:NEAR ; COMDATJ \
ГААВА 16. Практический пример: криптосистема RSA 361 _ТЕХТ SEGMENT _key_l$ = -516 _secret$ = -520 J PROC NEAR ;COMDAT ;9 :{ sub esp, 520 ;00000208H ;10 : CLINT keyj; ; 11 : USHORT secret; 8V -01 в O'i ; 18 : /* Переписываем переменные */ ; 19 : PURGEVARS_L ((2, sizeof (secret), &secret, sizeof (keyj), keyj)); lea eax, DWORD PTR _key_l$[esp+532] push eax lea ecx, DWORD PTR _secret$[esp+536] push 514 ;00000202H push ecx push 2 push 2 call _purgevars_l ;20 : ; 21 : return 0; xor eax, eax #r~ ;22 :}
362 Криптография на Си и C++ в действии add esp, 552 ;00000228Н ret 0 J ENDP _TEXT ENDS Для приложений, связанных с безопасностью, посоветуем еще использовать такой исчерпывающий механизм обработки ошибок, при котором даже в случае неверно заданного аргумента или в других исключительных ситуациях не разглашается никакая критическая информация. Точно так же необходимо принять меры для проверки подлинности кодов программ, реализующих криптографические приложения, чтобы предотвратить закладку троянских коней (или хотя бы обнаружить их), прежде чем программа будет запущена. Троянским конем (вспомним историю Троянской войны) называется программа, измененная так, что внешне она функционирует корректно, но дополнительно имеет нежелательный эффект, например, передает нарушителю через Интернет информацию о секретном ключе. Для решения указанных проблем на практике в криптографических приложениях зачастую используются «блоки безопасности» («security boxes», «S-блоки»). Реализующая их аппаратура защищена от атак «внедрения» и снабжена детекторами или сенсорами. Когда все эти ловушки нам удалось избежать, остается последняя опасность: а вдруг модуль будет разложен на множители? Эту угрозу также можно устранить, выбрав простые числа достаточно большими. Разумеется, пока не доказано, что не существует других методов взлома криптосистемы RSA, более легких, чем разложение. Кроме того, нет доказательства и того, что задача разложения больших чисел действительно сложная (как считается в настоящее время). Однако эти вопросы пока никак не сказались на использовании алгоритма: криптосистема RSA на сегодняшний день остается самой популярной асимметричной криптосистемой во всем мире, а ее применение в Интернете постоянно расширяется, особенно для аутентификации. Современные методы решения задачи о разложении предъявляют к простым числам, образующим модуль RSA, следующие требования: числа р и q должны быть достаточно большими, примерно одинаковыми по размеру, но, тем не менее, различаться в определенном числе двоичных разрядов, поскольку при p~q~ V и можно быстро найти разложение числа п, пробуя в качестве делителей п натуральные числа, близкие к у/п . В литературе часто рекомендуется в качестве р и q использовать так называемые сильные простые числа, позволяющие противостоять некоторым простым методам разложения. Простое число р называется сильным, если
ГЛАВА 16. Практический пример: криптосистема RSA 363 (а) число р - 1 имеет большой простой делитель г; (б) число р + 1 имеет большой простой делитель s\ (в) число г - 1 имеет большой простой делитель t. Высказываются разные мнения о влиянии сильных простых чисел на безопасность криптосистемы RSA. С недавних пор большинство сходится на том, что, хотя использовать простые числа не вредно, большой пользы от них тоже нет (см. [MOV], п. 8.2.3, а также [RegT], Приложение 1.4). Некоторые считают, что эти числа использовать вообще не стоит ([Schn], п. 11.5). Поэтому в реализованной нами программе мы обойдемся без сильных простых чисел. Для тех же, кому это интересно, приведем набросок процедуры генерации таких простых чисел: 1. Чтобы получить сильное простое число р длиной 1Р двоичных разрядов, сначала ищем такие простые числа s и t, что \og2s~\ogit~ j^-log2/p. Затем ищем такое простое число г, что г - 1 делится на t, последовательно проверяя на простоту числа вида г = к • It + 1, где к- 1, 2, ..., пока не получим простое число. Это обязательно произойдет не более чем через |_2 In 2t\ шагов (см. [HKW],CTp.418). 2. Теперь с помощью китайской теоремы об остатках (см. стр. 225) находим решение системы сравнений х = 1 mod г, х = -1 mod s: хо := 1 - 2r~ls mod rs, где число г"1 - мультипликативно обратное к г по модулю s. 3. Устанавливаем нечетное начальное значение: генерируем случайное число z, число разрядов которого близко к длине искомого числа р, но меньше нужного значения (это иногда обозначается символом g), и полагаем х0 <— х0 + z +rs - (z mod rs). Если число х0 четное, то полагаем х0 <— х0 + rs. По значению х0 начинаем определять р. Проверяем числа вида р = х0 + к • 2rs, где & = 0, 1, ..., пока мы не получим нужного числа цифр 1Р и число р не будет простым. Если ключ RSA должен содержать заданную открытую компоненту е, то стоит еще проверить, что НОД(р - 1, ё) = 1. Такое число р удовлетворяет всем необходимым условиям. Для проверки чисел на простоту пользуемся тестом Миллера-Рабина, реализованным в виде функции prime_l(). В любом случае, независимо от того, используются ли для ключей сильные простые числа, нужно запастись подходящей функцией, генерирующей простые числа заданной длины или из заданного интервала. Соответствующая процедура, дополнительно гарантирующая, что сгенерированное простое число р удовлетворяет условию НОД(р-1,/)= 1 для заданного числа/, приведена в [IEEE], стр. 73. Мы приведем слегка измененный алгоритм.
364 Криптография на Си и C++ в действии Алгоритм генерации простого числа/; такого, чтоРпш^Р ^Ртах 1. Сгенерировать случайное числортакое, чтортт<р<ртах. 2. Если р четное, то положить р <— р + 1. 3. Если р > ртлх, то положить р <— /?min + p mod (ртйХ + 1) и вернуть- и ся на шаг 2. 4. Вычислить d := НОД(р - 1,/) (см. п. 10.1). При d= 1 проверить р на простоту (см. п. 10.5). Если число р простое, то завершить алгоритм с результатом: р. Иначе положить /?<—/?+ 2 и вернуться на шаг 3. Реализацию этого алгоритма в виде функции на языке C++ читатель найдет в пакете FLINT/C (файл flintpp.cpp). 1——^—f—■ Функция: 1 Синтаксис: 'fTO Вход: Возврат: Генерация простого числа/? из интервала [рт\п,pmdX], дополнительно удовлетворяющего условию НОД(/> - 1,/) = 1, где число /- целое положительное нечетное const LINT findprime (const LINT& pmin, const LINT& pmax, const LINT& f); pmin: наименьшее допустимое значение pmax: наибольшее допустимое значение f: целое положительное нечетное число, которое должно быть взаимно простым с р - 1 простое число р типа LINT, проверенное вероятностным алгоритмом (см. п. 10.5), где НОД(р - 1,/) = 1 1 const LINT findprime (const LINT& pmin, const LINT& pmax, const LINT& f) { if (Ipmin.init) LINT::panic (E_LINT_VAL, "findprime", 1, LINE ); if (Ipmax.init) LINT::panic (E_LINT_VAL, "findprime", 2, _LINE_); if (pmin > pmax) LINT::panic (E_LINT_VAL, "findprime", 1, LINE ); if (If.lnit) LINT::panic (EJJNT_VAL, "findprime", 3, _LINE_); // 0 < f должно быть нечетным if (f.isevenQ) LINT::panic (E_LINT_VAL, "findprime", 3, _LINE_);
ГЛАВА 16. Практический пример: криптосистема RSA 365 LINT р = randBBS (pmin, pmax); LINT t = pmax-pmln; if (p.iseven()) { ++p; } if (p > pmax) { p = pmin + p % (t+ 1); } while ((gcd (p- 1, f) != 1) || !p.isprime()) { ++p; ++p; while (p > pmax) { p = pmin + p% (t + 1); if (p.iseven()) .^. { i ++p; } } Q>< } return p; } Кроме того, функцию findprime() можно перегрузить так, чтобы вместо границ pmin и/?тах задавать двоичную длину числа р. -&V.
Криптография на Си и C++ в действии Генерация простого числа р из интервала [2~ , 2 - 1], дополнительно удовлетворяющего условию НОД(р - 1,/) = 1, где число /- целое положительное нечетное const LINT findprime (const USHORT I, const L1NT& f); I: требуемая двоичная длина f: целое положительное нечетное число, которое должно быть взаимно простым с р - 1 простое число р типа LINT, где НОД(р - 1,/) = 1 Что касается выбора длины ключа, наиболее информативными здесь будут сведения о попытках разложения чисел на множители. В апреле 1996 года после нескольких месяцев совместной работы ряда университетов и исследовательских лабораторий США и Европы под руководством А. К. Ленстры (А. К. Lenstra) 4 для RSA-модуля RSA-130 = 18070820886874048059516561644059055662781025167 69401349170127021450056662540244048387341127590 812303371781887966563182013214880557 длины 130 десятичных разрядов было найдено разложение вида RSA-130 = 39685999459597454290161126162883786067576449112 810064832555157243 х 45534498646735972188403686897274408864356301263 205069600999044599. Затем в феврале 1999 года модуль RSA-140 = 21290246318258757547497882016271517497806703963 27721627823338321538194998405649591136657385302 1918316783107387995317230889569230873441936471 был разложен на два 70-разрядных множителя: RSA-140 = 33987174230284385545301236276138758356339864959 69597423490929302771479 х 62642001874012850961516549482644422193020371786 23509019111660653946049. Lenstra Arjen К.: Factorization of RSA-130 using the Number Field Sieve, http://dbs.cwi.nl.herman.NFSrecords/RSA-130; см. также [Cowi].
ГЛАВА 16. Практический пример: криптосистема RSA 367 Эти результаты были достигнуты группами исследователей из Нидерландов, Австралии, Франции, Великобритании и США под руководством Германа Дж. Дж. те Риля (Herman J. J. te Riele) из Национального научно-исследовательского института математики и информатики CWI (Centrum voor Wiskunde en Informatica) в Нидерландах. 5 Числа RSA-130 и RSA-140 взяты из списка 42 модулей RSA, опубликованного в 1991 г. компанией RSA Data Security, Inc. в помощь исследователям в области криптографии.б Вычисления по разложению RSA-130 и RSA-140 были распределены между большим числом рабочих станций, затем результаты были сопоставлены. Разложение числа RSA-130 заняло около 1000 MIPS-лет, RSA-140 - 2000 MIPS-лет.7 "^ Вскоре после этого, в конце августа 1999 года, мир был поражен известием о разложении числа RSA-155. Работы опять проводились интернациональной командой под руководством Германа те Риля и заняли около 8000 MIPS-лет. С разложением числа RSA-155 = 10941738641570527421809707322040357612003732945 44920599091384213147634998428893478471799725789 12673324976257528997818337970765372440271467435 31593354333897 на два 78-разрядных множителя RSA-155 = 10263959282974110577205419657399167590071656780 8038066803341933521790711307779 х 10660348838016845482092722036001287867920795857 5989291522270608237193062808643 был пересечен магический рубеж в 512 бит - ключи такой двоичной длины долгие годы считались надежными. Вопрос о том, ключи какой длины использовать для алгоритма RSA, пересматривается после каждого очередного сообщения об успехах в области разложения чисел. А. К. Ленстра (А. К. Lenstra) и Эрик Р. Верхуль (Eric R. Verheul) [LeVe] разработали модель определения длины ключа для различных типов криптосистем. Эта модель основана на ряде проверенных устоявшихся предположений и учитывает современные достижения в области разложения чисел. 5 Сообщение получено по электронной почте от Herman.te.Riele@cwi.nl по сети Number Theory Network 4 февраля 1999 г. См. также http://www.rsasecurity.com/rsalabs/html/status.html 6 http://www.rsasecurity.com/rsalabs/html/factoring.html. 7 MIPS = mega instmction per second (миллион инструкций в секунду) - единица измерения быстродействия компьютера. Считается, что со скоростью 1 MIPS работает компьютер, выполняющий 700 000 сложений и 300 000 умножений в секунду.
368 Криптография на Си и C++ в действии Результаты, представленные в виде таблицы (см. таблицу 16.1), позволяют определить минимальную длину ключа, рекомендуемую в недалеком будущем, для асимметричных криптосистем RSA, Эль-Гамаля и Диффи-Хеллмана. Таблица 16.1. Рекоменлуемая ллина ключа по Ленстре и Верхулю Год 2001 2005 2010 2015 2020 2025 Длина ключа (в битах) 990 1149 1369 1613 1881 2174 -М' Таким образом, чтобы сегодня обеспечить критическому приложению приемлемый «запас прочности», длина ключа RSA должна быть не менее 1024 бит. При этом следует помнить, что успехи в решении задачи разложения постепенно будут отодвигать эту границу, то есть за развитием науки надо следить. В зависимости от назначения, для особо чувствительных приложений могут потребоваться числа длины 2048 бит и более (см. [Schn], Глава 7, и [RegT], Приложение 1.4).8 Имея в распоряжении пакет FLINT/C, мы легко можем генерировать ключи такой длины. Нас не очень волнует, что сложность разложения падает с ростом скорости новых компьютеров - ведь на этих же компьютерах мы можем генерировать все более длинные ключи. Таким образом, можно считать, что безопасность криптосистемы RSA зависит в основном от развития методов разложения. Сколько же существует таких ключей? Хватит ли их, чтобы раздать каждому из живущих на Земле мужчин, женщин и детей (а может даже кошек и собак) хотя бы по одному ключу RSA? На эти вопросы дает ответ теорема о простых числах: число простых чисел, меньших заданного х, примерно равно х/\пх (см. стр. 244). Модуль длины 1024 генерируется как произведение двух простых чисел, длины 512 бит каждое. Таких чисел примерно 2512/512, то есть около 10 каждые два из них образуют модуль. Если обозначить /V= 10 , то существует N(N- 1)/2 таких пар, около 10300 различных модулей, причем каждое из этих чисел можно выбрать в качестве компонента секретного ключа. Чтобы было легче осознать всю мощь этого числа, скажем лишь, что вся обозримая Вселенная содержит «всего» 1080 элементарных частиц (см. [Saga], Глава 9). Или еще пример. Если каждому жителю Земли выдавать каждый день по десять новых Удобно выбирать длину ключей RSA в битах кратной 8, чтобы число байтов было целым.
ГЛАВА 16. Практический пример: криптосистема RSA 369 модулей, то модулей хватит на 10287 лет, при этом ни разу не будет повторений. Ну а сегодня нашей планете «всего» несколько миллиардов лет. И, наконец, очевидно, что произвольный текст можно представить в виде положительного целого числа. Сопоставляя каждой букве алфавита единственное число, текст можно представить в виде целого числа сколькими угодно способами. Традиционным является представление символов в ASCII (American Standard Code for Information Interchange)-кoдax. Рассматривая код каждого символа как разряд в системе счисления с основанием 256, можно сопоставить ASCII-закодированному тексту целое число. Вероятность при этом получить число М такое, что НОД(М, п) > 1, то есть что М будет делиться на один из делителей р или q ключа л, пренебрежимо 3 ■* мала. Если текст М слишком велик, чтобы служить в качестве цп модуля п или ключа, то есть больше, чем п - 1, то его можно разбить на блоки, численные значения Мь М2, М3, ... которых будут уже меньше, чем п. Каждый из этих блоков зашифровывается по отдельности. Если текст сообщения достаточно длинный, то указанный процесс становится весьма утомительным, поэтому RSA редко используется для шифрования длинных текстов. Для этого лучше подойдет симметричный криптоалгоритм (например тройной DES, IDEA или Rijndael; см. главу 19 и [Schn], главы 12, 13, 14), выполняющий шифрование гораздо быстрее с неменьшей стойкостью. Больше всего алгоритм RSA подходит для зашифрования ключа симметричного криптоалгоритма, передаваемого по каналам связи. 16.3. Цифровая подпись RSA «С позволения Вашего Величества, - сказал Валет, - я этого письма не писал, и они этого не докажут. Там нет подписи». Льюис Кэррол, Приключения Алисы в Стране Чудес Объясним теперь, как используется RSA для формирования цифровой подписи. Пусть участник А отправляет участнику В сообщение М, подписанное цифровой подписью, а В проверяет правильность этой подписи. 1. Участник А генерирует компоненты яд, ^д и ед ключа RSA и передает участнику В свой открытый ключ {еь, па). 2. Участник А хочет отправить участнику В сообщение М, подписанное цифровой подписью. Для этого А вырабатывает значение R = \х(М), ■&fU\ - ЯП,
I 70 Криптография на Си и C++ в действии I где R < «а, с помощью функции избыточности ц (см. ниже). Затем I А вычисляет подпись [ S = R(iA mod па I и отправляет участнику В пару (М, 5). I 3. У участника В уже есть открытый ключ (ед, ид) участника А. Полу- I чив от него сообщение М и подпись 5, В вычисляет I R = viM), I R' = S€k mod лА I с помощью открытого ключа (ед, яд). I 4. Наконец, В проверяет равенство R' = R. Если оно выполняется, то В i считает подпись участника А правильной. В противном случае под- I пись считается неверной. I Цифровая подпись, для проверки которой необходимо передавать и I само сообщение М, называется цифровой подписью с приложением. Г Цифровые подписи с приложением используются в основном для I сообщений переменной длины, численное значение которых пре- i вышает модуль, то есть М > п. В принципе, можно, конечно, как мы I " это делали выше, разбить сообщение на блоки Мь М2, М3, ... под- I ходящей длины М, < п, а затем зашифровать и подписать каждый I блок в отдельности. Оставим в стороне вопрос о том, что сообще- I , ние можно «перемешать», просто изменив порядок блоков и под- I писей. Здесь есть более веские причины, побуждающие нас вместо I блоков использовать функцию |я, которую мы назвали функцией I избыточности. I Во-первых, функция избыточности ju,: М —> Жп отображает произ- | вольное сообщение М из пространства сообщений М в кольцо клас- i сов вычетов Z„, в соответствии с чем сообщения обычно сжимаются с помощью хэш-функции (см. стр. 373) до значений z й 2160. Затем такому значению сопоставляется заранее заданная последователь- ['■■" ность символов. Для вычисления значения \х(М) требуется выполнить один шаг процедуры RSA, а значение хэш-функции вычисля- | ется быстро в соответствии с своим назначением, поэтому второй [ вариант гораздо предпочтительнее с точки зрения скорости. , Во-вторых, алгоритм RSA обладает следующим «нехорошим» для цифровых подписей свойством: любые два сообщения М\ и М2 связаны мультипликативным соотношением (16А) (M,M2)d mod п = (MdxMd2) mod n , позволяющим подделать подпись, если, конечно, не предприняты никакие меры защиты.
ЛАВА 16. Практический пример: криптосистема RSA 371 Благодаря этому свойству - гомоморфизму - функции RSA можно, не используя избыточность R, получать сообщения со «скрытой» подписью. Пусть нарушитель выбрал секретное сообщение М и с помощью некоторого безобидного сообщения Mi сформировал еще одно сообщение М2 := ММ\ mod пд. Некий пользователь или удостоверяющий центр А подписывает ему сообщения М\ и М2: Sj =MfA mod/iA, S2 = М%к modnK . С помощью этих подписей нарушитель может вычислить подпись S2S^1 mod nK для сообщения М, которое А, конечно же, подписывать не собирался, да он и не будет об этом знать, формируя подписи Sj и 52. В этом случае говорят, что у сообщения М скрытая подпись. Разумеется, с большой вероятностью сообщение М2 не будет содержать никакого осмысленного текста, и, кроме того, А настоятельно рекомендуется не подписывать никакие М\ и М2, не ознакомившись с их содержанием, тем более по просьбе незнакомых лиц. И все же не стоит уповать на человеческое благоразумие и говорить потом о недостатках криптографического протокола, тем более что эти недостатки можно устранить, например, введя в сообщение избыточность. Для функции избыточности ji должно выполняться условие '16.5) \i(MxM2) Ф \х(М{)\х(М2) для всех М[, М2£ М, тогда сама функция подписи не будет обладать нежелательным свойством гомоморфизма. Помимо подписей с приложением существуют другие методы, в которых подписанное сообщение извлекается из самой подписи, - это так называемые цифровые подписи с восстановлением сообщения (см. [MOV], глава 11, [IS02] и [IS03]). Цифровые подписи с восстановлением сообщения, основанные на алгоритме RSA, особенно хороши для коротких сообщений, двоичная длина которых меньше половины двоичной длины модуля. В любом случае нужно тщательно исследовать безопасность функции избыточности. В 1999 году Корон (Согоп), Наккаче (Naccache) и Штерн (Stern) опубликовали способ атаки на такие функции, который заключается в следующем. Нарушитель набирает достаточное количество подписей RSA, соответствующих сообщениям, целочисленное представление которых делится исключительно на маленькие простые числа. При такой структуре сообщений нарушитель при благоприятных условиях может, не зная ключа подписи, формировать новые подписи и выдавать их за подлинные (см. [Сого]). На это открытие незамедлительно отреагировала ISO: в октябре 1999 года рабочей группой SC 27 стандарт [IS02] был изъят из обращения со следующим уведомлением:
f2 Криптография на Си и C++ в действии I «Учитывая многочисленные атаки на схемы цифровой подписи I RSA ..., ISO/IEC JTC 1/SC 27 приняли единогласное решение: [ стандарт IS 9796:1991 больше не обеспечивает независимым от I приложений цифровым подписям достаточной защиты и должен L быть отменен» 9. I Отмена стандарта касается цифровых подписей, в которых функция I RSA применена непосредственно к короткому сообщению. К подпи- I сям с приложением, использующим хэш-функцию, это не относится. I; Широкое распространение получила схема включения избыточности I формата PKCS #1 от RSA laboratories, для которой атака Корона, I Наккаче и Штерна имеет в лучшем случае теоретическое значение I и не представляет реальной угрозы (см. [RDS1], [Сою], стр. 11-13 и I [RDS2]). Формат PKCS #1 определяет вид так называемого блока I зашифрования ЕВ, подаваемого на вход оператора зашифрования I или формирования цифровой подписи: I EB = BT||PS1||...||PS/||00||D1||...||Dw. I В начале стоит байт ВТ, указывающий тип блока (01 для операций I с секретным ключом, т.е. формирования подписи; 02 для операций I с открытым ключом, т.е. зашифрования). Затем следуют не менее I восьми байтов-заполнителей PSb ..., PS/, /> 8, имеющих значения [ FF (в шестнадцатиричном виде) в случае формирования подписи и случайные ненулевые значения в случае зашифрования. За ними | идет байт-разделитель 00 и, наконец, байты данных Db ..., D„ - так К сказать, полезная нагрузка. Число / байтов-заполнителей PS, зави- I сит от размера модуля т и числа п байтов данных. Если значение к I таково, что V6.6) 28(*-,} <т< 28*, I то \16.7) 1 = к-2-п. Минимально значение 8 <7 байтов-заполнителей выбрано для зашифрования из соображений безопасности. Даже для маленького г сообщения нарушитель не сможет зашифровать все возможные г значения и сравнить результат с данной шифрограммой, надеясь I определить открытый текст без знания секретного ключа. Здесь I важно, чтобы значения PS/ были случайными и определялись зано- Г во для каждой операции зашифрования. Для единообразия то же I минимальное число байтов-заполнителей сохранено и для подписи, I откуда следует неравенство для числа п байтов данных: ISO/IEC JTC 1/SC27: Recommendation on the withdrawal of IS 9796:1991, 6 October 1991.
ГЛАВА 16. Практический пример: криптосистема RSA 373 (16.8) п<к-\0. В случае подписи байты данных D, обычно включают в себя иден- ■•'• тификатор хэш-функции Я и ее значение (хэш-образ) Н(М), представляющее подписываемое сообщение М. Итоговая структура данных называется Digestlnfo. В этом случае число байтов данных ;, определяется постоянной длиной хэш-образа и не зависит от длины текста, что особенно выгодно, если М намного длиннее, чем Н(М). Мы не будем вдаваться в подробности построения структуры Digestlnfo, а просто будем считать, что байты данных соответству- м ют значению Н(М) (см. по этому поводу [RDS1]). Ь' С точки зрения криптографии хэш-функция должна обладать рядом основных требований, чтобы не снижать безопасность соответст- 1 ( вующей функции избыточности и тем самым поставить под сомнение всю процедуру подписи. Исследуя применение хэш-функций и функций избыточности в цифровой подписи, можно заметить ., следующее. ( До сих пор мы предполагали, что цифровая подпись с приложением ";{ связана с избыточностью R = ц(М), основным компонентом кото- "' рой является хэш-образ подписываемого сообщения. Два текста М и М', для которых Н(М) = Н(М') и, следовательно, \х(М) = ji(M'), будут иметь одну и ту же подпись S = Rd = \x(M)d = [х(М')(1 mod n. .; Отсюда получатель подписи S к сообщению М может заключить, что эта подпись на самом деле относится к сообщению М', что, з скорее всего, не входило в планы отправителя. Аналогично, отпра- .; витель может думать, что он подписывает текст М\ на самом деле г подписывая М. А дело здесь в том, что всегда существуют тексты М Ф М\ для которых Н(М) = Н(М'), поскольку бесконечное мно- о, жество текстов отображается в конечное множество хэш-образов. ,* Это та цена, которую мы платим за фиксированную длину хэш- образов. 10 Г Поскольку мы предполагаем, что для данной хэш-функции или "S" функции избыточности всегда существуют тексты, обладающие ^ одинаковыми подписями (для одного и того же ключа подписи), -v встает вопрос о том, чтобы эти тексты трудно было найти или fi> построить. Итак, хэш-функция должна быть легко вычислимой, в отличие от обратного к ней преобразования. То есть, для данного значения Я хэш-функции должно быть трудно найти прообраз, отображающийся в Я. Функции, обладающие таким свойством, называются однонаправленными (или вычислимыми в одну сторону). Хэш-функция На языке математики мы бы сказали, что хэш-функция Н: М —> Z„, отображающая тексты произвольной длины в элементы множества Z„, не инъективна.
174 Криптография на Си и C++в действии I должна быть свободной от коллизий, то есть для данного хэш- I значения должно быть трудно найти два разных прообраза. На I сегодняшний день такими свойствами обладают такие мощные I функции как RIPEMD-160 (см. [DoBP]) и Secure Hash Algorithm I SHA-1 (см. [ISOl]). I Позволим себе не углубляться дальше в эту столь важную для I криптографии тему. Заинтересованному читателю советуем обра- I титься к [Ргеп] или [MOV], глава 9, см. также ссылки в этих работах. 1 Алгоритмы преобразования текстов или хэш-образов в натураль- I ные числа можно найти в [IEEE], глава 12 «Методы шифрования» I (хотя у нас уже есть соответствующие функции clint2byte_l() и I byte2clint_l(); см. стр. 172). Реализацию алгоритма RIPEMD-160 I читатель найдет в файле ripemd.c на компакт-диске. I* При ближайшем рассмотрении описанного выше протокола подпи- I си мы немедленно задаемся вопросом: откуда В может знать, что I ему прислали аутентичный (подлинный) открытый ключ участ- I ника А? Не будучи в этом уверен, В не может доверять и соответ- I ствующей подписи, даже если она удовлетворяет проверочному 1 соотношению. Эта проблема особенно актуальна, если А и В не I знакомы друг с другом или если они не обменивались открытыми I ключами лично, что обычно и бывает при взаимодействии через I Интернет. I Для того чтобы В все же смог доверять цифровой подписи, А дол- I жен предоставить ему сертификат от сертифицирующего органа, 1 подтверждающий подлинность открытого ключа. Неформальная I «расписка», которой можно верить, а можно и не верить, здесь, I конечно же, не подходит. Сертификат - это набор данных, формат I которых соответствует определенному стандарту. и Эти данные, 1 помимо всего прочего, несут информацию и об участнике А, и о его I открытом ключе, да и сами подписаны цифровой подписью серти- I фицирующего органа. I Информация, содержащаяся в сертификате, позволяет проверить I подлинность ключей участников. Уже есть программные приложе- I ния, поддерживающие такую проверку. О будущем разнообразии I таких приложений, в связи с развитием так называемой инфра- I структуры открытых ключей (public key infrastructure, PKI), пока I можно только догадываться. Сегодня они применяются для цифро- I вой подписи сообщений электронной почты, при проверке ком- I мерческих транзакций, в электронной и мобильной коммерции I (т-соттегсе), в электронном документообороте и управлении Г (см. рис. 16.1). Часто используется стандарт ISO 9594-8 или, что то же самое, рекомендация X.509v3 от ITU (ранее CCITT).
ГЛАВА 16. Практический пример: криптосистема RSA 375 Рисунок 16,1. Структура сертификата Version (Указывает на версию сертификата, например, V3) Serial Number (Уникальный целочисленный идентификатор для сертификата) Signature (Идентификатор алгоритма, используемого для подписи сертификата) Issuer Name (Имя органа, выдавшего сертификат, согласно Х500) Validity (Срок действия сертификата) Subject Name (Имя владельца согласно Х.500) Subject Public Key Info (Открытый ключ владельца) Issuer Unique Identifier (Уникальный идентификатор сертифицирующего органа (необязательно)) Subject Unique Identifier (Уникальный идентификатор владельца (необязательно)) Цифровая подпись серти- Extensions (Дополнительная информация) фицирующего органа I 1010010011110... | | 10011010011010... | Секретный ключ сертифицирующего органа Если участник В знает открытый ключ сертифицирующего органа, то он может проверить сертификат, предъявленный участником А, а значит, и цифровую подпись участника А, и тем самым убедиться в подлинности информации. Этот процесс показан на рис. 16.2, где клиент получает извещение, подписанное цифровой подписью банка, и сертификат банка, предоставленный удостоверяющим центром. Рисунок 16.2. Сертификация цифровой полписи 'ИГ I Version Serial Number Signature Issuer Name Validity ©=£ Subject Name Issuer Identifier 110111011110110001100 0001111000001101110... ^ /я\ и --— P (Qh-tf Открытый ключ банка Ь ммар Сертификат банка W3-Bank Состояние счета Имя: Браузер, Бернард Счет: 1234567890 Баланс: $4286,37 Дата: 14.06.2000 Подпись: 11101011011010011100 011101010011100111... Извещение банка, подписанное цифровой в проверяет сертификат, предоставленный банком, и с помощью открытого ключа банка проверяет цифровую подпись банка
376 Криптография на Си и C++ в действии Такая форма извещения хороша тем, что извещение может быть передано клиенту по любому электронному каналу связи (например, по электронной почте), при этом его следует предварительно зашифровать, чтобы сохранить конфиденциальность информации. И все же проблема доверия никуда чудесным образом не исчезла, а просто немного видоизменилась: теперь участник В должен проверять подлинность не ключа участника А (то есть банка в примере выше), а сертификата, представленного участником А. Подлинность сертификатов нужно устанавливать заново при каждом появлении как нового владельца сертификата, так и выдавшего ему сертификат сертифицирующего органа. Это можно сделать лишь при следующих условиях: • открытый ключ сертифицирующего органа известен; • сертифицирующий орган безупречно идентифицирует получателей сертификатов и защищает их секретные сертификационные ключи. Для достижения первой цели открытый ключ сертифицирующего органа можно сертифицировать у другого, вышестоящего сертифицирующего органа и т. д., то есть нужна иерархия сертифицирующих органов и сертификатов. При такой структуре предполагается, что открытый ключ самого высокого, корневого сертифицирующего органа известен и может считаться подлинным. То есть доверие к такому ключу должно подкрепляться другими средствами, в большей степени техническими и организационными мерами. Второе условие, разумеется, выполняется для всех сертифицирующих органов в указанной иерархии. Для придания подписи законной силы сертифицирующий орган должен предпринять технические и организационные меры, предписанные законом или иными документами. В конце 1999 года Европейским Союзом была принята директива, устанавливающая схему использования цифровых подписей в Европе (см. [EU99]). Предполагается также выработка положения, которое объединило бы в себе различные национальные подходы к цифровой подписи и послужило бы толчком к созданию Европейского стандарта подписи, имеющего статус национального закона. Пересмотренный закон о подписи в Германии намечен на 2001 год (см. [SigG]). В США закон об электронных подписях (Electronic Signatures Act) действует с октября 2000 года. Все эти законодательные акты позволяют надеяться, что в ближайшем будущем на основе многочисленных цифровых подписей разных стран будет выработан единый механизм, который позволит надежно заверять транзакции по всей Европе, а спустя некоторое время (почему бы и нет?) между Европой и Америкой. А сейчас оставим эту интересную тему, обсуждение которой читатель найдет на страницах [Bies], [Glad], [Adam], [Mied] и [Fegh], и обратимся, наконец, к разработке на C++ классов функций, реализующих процессы зашифрования и расшифрования сообщений, также формирования и проверки цифровой подписи.
ГЛАВА 16. Практический пример: криптосистема RSA 377 RSA-классы на C++ В этом параграфе мы напишем на C++ класс RSAkey, включающий в себя следующие функции: • RSAkey: :RSAkey() - генерация ключа криптосистемы RSA; • RSAkey: :export() - экспорт открытых ключей; • RSAkey::decrypt() - расшифрование; • RSAkey::sign() - формирование цифровой подписи с использованием хэш-функции RIPEMD-160; а также класс RSApub для хранения и применения открытых ключей, содержащий функции • RSApub::RSApub() - импорт открытого ключа из объекта класса RSAkey; • RSApub::crypt() - зашифрование текста; • RSApub::authenticate() - проверка цифровой подписи. Идея заключается в том, чтобы оперировать с криптографическими ключами не просто как с числами, обладающими определенными криптографическими свойствами. Эти ключи мы будем рассматривать как объекты, подсказывающие пути их применения, делая их доступными внешнему миру и в то же время защищая персо- > "J' нальные данные от прямого доступа. К объектам класса RSAkey относятся открытая и секретная компоненты ключа RSA (закрытые данные), а также общедоступные функции расшифрования и формирования подписи. Функции-конструкторы позволяют генерировать ключи: • фиксированной длины со встроенной инициализацией генератора случайных чисел BBS; • регулируемой длины со встроенной инициализацией генератора случайных чисел BBS; • регулируемой длины с передачей через вызываемую программу начального значения типа LINT для генератора случайных чисел BBS. К объектам класса RSApub относится открытый ключ, импортируемый из объекта класса RSAkey, а также общедоступные функции зашифрования и проверки подписи. То есть при создании объекта класса RSApub уже должен существовать и быть инициализированным объект класса RSAkey. В отличие от объектов класса RSAkey, объекты класса RSApub не считаются конфиденциальными, при работе с ними допускается больше свободы. Объекты же класса RSAkey, особенно в серьезных приложениях, могут храниться или передаваться только в зашифрованном виде или будучи защищен- * ными специальными аппаратными средствами. О] ■
378 Криптография на Си и C++ в действии Прежде чем строить эти классы, введем некоторые ограничения, которые позволят нам оставаться в разумных пределах времени и памяти. Для простоты будем считать входное значение процедуры зашифрования RSA всегда меньше модуля, то есть не нужно будет разбивать входной текст на блоки. Кроме того, оставим в стороне более трудоемкие функции функционирования и безопасности, которые приходится решать при реализации полноценных классов криптосистемы RSA (см. в этой связи стр. 357). Не будем, однако, забывать о необходимости быстро реализовать процедуры расшифрования и формирования подписи. Китайская г теорема об остатках (см. стр. 225) позволяет выполнять операции с секретным ключом d почти в четыре раза быстрее, чем при обычном способе возведения в степень. Пусть (d, n) - секретный ключ RSA, где п =pq. Положим dp := d mod (р - 1), dq := d mod (q - 1) и расширенным алгоритмом Евклида найдем представление ' J 1 = rp + sq, где значение г мультипликативно обратно к р по модулю q (см. п. 10.2). Теперь с помощью чисел р, q, dp, dq, r можно вычислить с = md mod n: 1. Вычислить ax<r-m p modp и а2 <— т ч modg . 2. Положить с <r-ai+ p((a2 - ci\)r mod q). После шага 1 получаем а{ = т р = md mod р и а2 =т ч = tnd mod q Чтобы это увидеть, достаточно вспомнить малую теорему Ферм (см. стр. 198), согласно которой тГ{ = 1 mod p и mq~l = I mod q со ответственно. Из равенства d = lip ~ 1) + dp с целым / следует, что (16.9) т* s w'Cp-^ s {mp~^mdp s mdp mod p , аналогичное выражение получаем и для md mod q. Применяя алг ритм Гарнера (см. стр. 229) с т\ :=р, т2 :=q и г := 2, сразу получаем что значение с на шаге 2 и есть искомое решение. Быстрое расшиф рование осуществляется с помощью вспомогательной функци RSAkey::fastdecrypt(). Все операции возведения в степень по моду лям р, qwn выполняются с использованием алгоритма Монтгомер функцией LINT::mexpkm() (см. стр. 312). // Взято из файла rsakey.h #include "flintpp.h" #include "ripemd.h" #define BLOCKTYPE_SIGN 01 #deflne BLOCKTYPE_ENCR 02
ГЛАВА 16. Практический пример: криптосистема RSA 379 // Структура ключа RSA со всеми ключевыми компонентами typedef struct { LINT kpub, kpriv, mod, p, q, ep, eq, r; USHORT bitlenjnod; // длина модуля в битах USHORT bytelen_mod; // длина модуля в байтах JKEYSTRUCT; II Структура, содержащая компоненты открытого ключа typedef struct { < LINT kpub, mod; USHORT bitlenjnod; // длина модуля в битах USHORT bytelen_mod; // длина модуля в байтах } pkeystruct; class RSAkey { public: inline RSAkey (void) {}; RSAkey (const int); RSAkey (const int, const LINT&); pkeystruct export_public (void) const; UCHAR* decrypt (const LINT&, int*); LINT sign (const UCHAR* const, const int); private: keystruct key; // Вспомогательные функции ■жипд"& int makekey (const int); int testkey (void); LINT fastdecrypt (const LINT&); }; class RSApub {
380 Криптография на Си и C++ в действии public: inline RSApub (void) {}; RSApub (const RSAkey&); LINT crypt (const UCHAR* const, const int); int authenticate (const UCHAR* const, const int, const LINT&); private: pkeystruct pkey; BPO-- }; // Взято из модуля rsakey.cpp #include "rsakey.h" llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll II Функции-члены класса RSAkey // Конструктор для генерации ключей RSA заданной двоичной длины RSAkey::RSAkey (const int bitlen) { int done; seedBBS ((unsigned longtime (NULL)); do { done = RSAkey::makekey (bitlen); } while (Idone); } // Конструктор для генерации ключей RSA заданной двоичной длин // с инициализацией генератора случайных чисел randBBS() // заданным аргументом типа LINT RSAkey::RSAkey (const int bitlen, const LINT& rand) { int done;
ГЛАВА 16. Практический пример: криптосистема RSA за1 seedBBS (rand); do { done = RSAkey::makekey (bitlen); } while (!done); } // Функция, экспортирующая компоненты открытого ключа pkeystruct RSAkey::export_public (void) const { pkeystruct pktmp; pktmp.kpub = key.kpub; pktmp.mod = key.mod; pktmp. bitlen_mod = key.bitlen_mod; pktmp. bytelen_mod = key.bytelen_mod; return pktmp; } // Расшифрование по RSA UCHAR* RSAkey::decrypt (const LINT& Ciph, int* LenMess) { // Расшифрование и преобразование открытого текста в вектор байтов UCHAR* Mess = Iint2byte (fastdecrypt (Ciph), LenMess); // Берем расшифрованные данные из блока зашифрования PKCS #1 return parse_pkcs1 (Mess, LenMess); } // Выработка цифровой подписи по RSA LINT RSAkey::sign (const UCHAR* const Mess, const int LenMess) { int LenEncryptionBlock = key.bytelen_mod - 1; UCHAR* EncryptionBlock = new UCHAR[LenEncryptionBlock]; if (NULL == format_pkcs1 (EncryptionBlock, LenEncryptionBLock, BLOCKTYPE_SIGN,
382 Криптография на Си и C++ в действии ripemdl 60 ((UCHAR*)Mess, (ULONG)LenMess), RMDVER » 3)) { delete [] EncryptionBlock; return LINT (0); // Ошибочный формат: слишком длинное сообщение } // заменяем блок зашифрования на значение типа LINT (конструктор 3) LINT m = LINT (EncryptionBlock, LenEncryptionBlock); delete [] EncryptionBlock; return fastdecrypt (m); } llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll II Закрытые вспомогательные функции класса RSAkey //... помимо всего прочего: генерация ключа RSA в соответствии с IEEE P1363, Annex A int RSAkey::makekey (const int length) { // Генерируем простое р такое, что 2Л(т - г - 1) <= р < 2л(т - г), где // т = [.(length + 1)/2j, а г - случайное число из интервала 2 <= г < 13 USHORT m = (length + 1) » 1 - 2 - usrandBBSJ () % 11 ; key.p = findprime (m, 1); // Определяем границы qmin и qmax интервала для простого числа q // Полагаем qmin = L(2A(length - 1))/р + 1J LINT qmin = LINT(0).setbit (length - 1)/key.p + 1; // Полагаем qmax = |_(2Alength)/p)J LINT qmax = LINT(0).setbit (Iength)/key.p; // Генерируем простое число q > p нужной длины: qmin <= q <= qmax key.q = findprime (qmin, qmax, 1);
ГЛАВА 16. Практический пример: криптосистема RSA 383 // Вычисляем модуль p*q длины 2A(length - 1) <= p*q < 2Alength key.mod = key.p * key.q; // Вычисляем функцию Эйлера LINT phi_n = key.mod - key.p - key.q + 1; // Генерируем открытый ключ длины 64 бита key.kpub = randBBS (64) | 1; while (gcd (key.kpub, phi_n) != 1) { ++key.kpub; ++key.kpub; } // Генерируем секретный ключ key.kpriv = key.kpub.inv (phi_n); // Генерируем секретные компоненты для быстрого расшифрования key.ep = key.kpriv % (key.p - 1); key.eq = key.kpriv % (key.q - 1); key.r = inv (key.p, key.q); return testkey(); } // Функция для тестирования int RSAkey::testkey (void) { LINT mess = randBBS (Id (key.mod) » 1); return (mess == fastdecrypt (mexpkm (mess, key.kpub, key.mod))); } // Быстрое расшифрование по RSA LINT RSAkey::fastdecrypt (const LINT& mess) { LINT m, w; m = mexpkm (mess, key.ep, key.p); w = mexpkm (mess, key.eq, key.q); w = w.msub (m, key.q);
384 Криптография на Си и C++ в действии w = w.mmul (key.r, key.q) * кеу.р; return (w + m); } llll/IIIIIIIIIIIIIItll!IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIHIII/llll II Функции-члены класса RSApub // Конструктор RSApub() RSApub::RSApub (const RSAkey& k) { // Импортируем открытый ключ из к pkey = k.export(); } // Зашифрование по RSA bwqgtyuj LINT" RSApub::crypt (const UCHAR* const Mess, const int LenMess) { int LenEncryptionBlock = key.bytelen_mod - 1; UCHAR* EncryptJonBlock = new UCHAR[LenEncryptionBlock]; // Форматируем блок зашифрования в соответствии с PKCS #1 if (NULL == format_pkcs1 (EncryptJonBlock, LenEncryptionBlock, BLOCKTYPE_ENCR, Mess, .^ -: ^ (ULONG)LenMess)) { delete [] EncryptJonBlock; return LINT (0); // Ошибочный формат: слишком длинное сообщение } // Преобразуем блок зашифрования в значение типа LINT (конструктор 3) LINT m = LINT (EncryptJonBlock, LenEncryptionBlock); delete [] EncryptJonBlock;
ГЛАВА 16. Практический пример: криптосистема RSA 385 return (mexpkm (m, pkey.kpub, pkey.mod)); } // Проверка цифровой подписи по RSA jnt RSApub::authenticate (const UCHAR* const Mess, const int LenMess, const LINT& Signature) lo . { 1 int I, verification = 0; UCHAR* m = Iint2byte (mexpkm (Signature, pkey.kpub, pkey.mod), &l); UCHAR* h = ripemd160 ((UCHAR*)Mess, (ULONG)LenMess); // Берем данные из расшифрованного блока зашифрования PKCS #1 m = parse_pkcs1 (m, &l); // Сравниваем длину и значение расшифрованного текста со значением хэш-функции if (I == (RMDVER » 3)) { ,rv;, verification = Imemcmp ((char*)h, (char*)m, RMDVER » 3); } return verification; } Классы RSAkey и RSApub содержат еще несколько операторов, которые мы здесь не обсуждаем: ! 4Х" RSAkey& operator (const RSAkey&); iV' friend int operator== (const RSAkey&, const RSAkey&); friend int operator!= (const RSAkey&, const RSAkey&); friend fstream& operator« (fstream&, const RSAkey&); friend fstream& operator» (fstream&, RSAkey&); и RSApub& operator= (const RSApub&); 13- 1697
386 Криптография на Си и C++ в действии friend int operator== (const RSApub&, const RSApub&); friend int operator!= (const RSApub&, const RSApub&); friend fstream& operator« (fstream&, const RSApub&); friend fstream& operator» (fstream&, RSApub&); Это операторы поэлементного присваивания, проверки равенств неравенств, считывания ключей из внешнего носителя памяти и за писи ключей на него. Здесь следует помнить, что секретный ключ, как и открытый, хранится в открытом виде. При практической pea лизации секретные ключи нужно хранить в зашифрованном виде. Есть еще функции RSAkey::purge (void), RSApub::purge (void), позволяющие перезаписывать ключи и устанавливать соответст вующие LINT-компоненты в 0. Форматирование блоков сообщени для зашифрования или формирования подписи в соответствии с спецификацией PKCS #1 осуществляется функцией UCHAR* format_pkcs1 (const UCHAR* ЕВ, const int LenEB, const UCHAR BlockType, const UCHAR* Data, const int LenData). Для анализа блоков расшифрованного сообщения с проверко формата и извлечением полезных данных служит функция UCHAR* parse_pkcs1 (const UCHAR* EB, int* LenData). Классы RSAkey и RSApub можно расширять как угодно. Например, составить конструктор, который принимал бы открытый ключ в качестве параметра и вырабатывал соответствующий модуль и сек ретный ключ. При практической реализации могут понадобитьс дополнительные хэш-функции. Нужна и функция разбиения сооб щений на блоки. Этот список можно продолжать до бесконечности, что выходит за рамки данной книги. Тестовый пример для классов RSAkey и RSApub читатель найдет модуле rsademo.cpp в библиотеки FLINT/C. Программа транслиру ется с помощью дсс -02 -DFLINT_ASM -о rsademo rsademo.cpp rsakey.cpp flintpp.cpp flint.c ripemd.c -Iflint -lstdc++ при использовании, например, GNU С/С++-компилятора дсс по Linux и ассемблерных функций в библиотеке libflint.a.
ГЛАВА 17. Сделайте это сами: Протестируйте LINT 90% времени уходит на написание 10% кода. Роберт Седжевик, Алгоритмы Мы уже обсуждали тему тестирования в главе 12, в которой мы подвергли серьезной статической и динамической проверке основные арифметические функции из первой части книги. Сейчас нам нужна подобная обработка для проверки правильности класса LINT языка C++. Кроме этого, нам до сих пор необходимо обеспечить тестирование теоретико-числовых функций С. Подход статических тестов можно перенести прямо в класс LINT, в котором такой инструмент как PC-lint (см. [Gimp]), используемый для статического анализа функций С, может сослужить нам хорошую службу, поэтому мы можем применять его для проверки на синтаксическую грамотность и (в определенных рамках) на семантическую достоверность элементов класса LINT. Также нам предстоит рассмотреть функциональные особенности реализации нашего класса: нам необходимо показать то, что методы, содержащиеся в классе LINT, возвращают верные результаты. Тот способ, который мы использовали ранее, когда для выяснения корректности применяли эквивалентные или взаимно обратные операции, вне всяких сомнений, может использоваться и функциями C++. В следующем примере данный способ реализован функцией testdist(), которая объединяет сложение и умножение с помощью закона дистрибутивности. Даже здесь можно заметить, насколько меньше в ней синтаксически сложных операций по сравнению с функцией проверки в С. Ядро этой функции состоит из двух строчек кода! #include <stdio.h> #include <stdlib.h> #include "flintpp.h" ^m ;e.., void report_error (LINT&, LINT&, LINT&, int); void testdist (int); #define MAXTESTLEN CLINTMAXBIT #define CLINTRNDLN (ulrand64J()% (MAXTESTLEN + 1)) main() 13*
388 Криптография на Си и C++ в действии { testdist (1000000); } void testdist (int nooftests) { LINT a; LINTb; LINTc; int i; for (i = 1; i < nooftests; i++) { a = randl (CLINTRNDLN); b = randl (CLINTRNDLN); с = randl (CLINTRNDLN); // проверка + и * применением дистрибутивного закона if ((a + b)*c != (а*с + b*c)) report_error (a, b, с, LINE ); } } void report_error (LINT& a, LINT& b, LINT& c, int line) { LINTd = (a + b)*c; LINT e = a * с + b * c; cerr« "Ошибка в дистрибутивном законе в строке" « line « endl; cerr« "а = " « а « endl; cerr« "b = " « b « endl; cerr« "(a + b) * с = " « d « endl; cerr« "a * с + b * с = " « e « endl; abort(); }
ГЛАВА 17. Сделайте это сами: Протестируйте И NT 389 А теперь мы предоставим читателю в качестве упражнения проверить все операторы LINT таким же или подобным образом. Для того чтобы двигаться в нужном направлении, можно взглянуть на тестовую программу функций языка С. Однако существует несколько новых аспектов, которые предстоит рассмотреть: такие -о*?' как префиксные и постфиксные операторы ++ и - - соответственно, в равной степени, как оператор = =. Ниже приведены несколько дополнительных примечаний: • Тестирование программы обработки ошибок panic() со всеми определенными ошибками, без и с исключениями; • Тестирование функций ввода/вывода, потоковых операторов и манипуляторов; • Тестирование арифметических и теоретико-числовых функций. Теоретико-числовые функции можно проверить согласно принципам, схожим с арифметическими функциями. Для этих целей также хорошо подойдет использование обратных функций, эквивалентных функций или же различные реализации одной и той же функции, но независимые друг от друга насколько это возможно. Мы приведем примеры каждого из этих вариантов: • Если символ Якоби показывает, что элемент конечного кольца является квадратом, то этот факт можно проверить, взяв квадратный корень. И, наоборот, вычисленный квадратный корень может быть проверен простым возведением в квадрат по модулю. • Функцию inv() для вычисления инверсии / относительно умножения целого числа а по модулю п можно проверить с условием, что ai = 1 mod n. • Для вычисления наибольшего общего делителя двух целых чисел можно использовать две функции FLINT/C gcd_l() и xgcd_l(), последняя из которых возвращает представление наибольшего общего делителя как линейную комбинацию аргументов. Результаты можно будет сравнить один с другим, а полученная линейная комбинация также должна быть согласована с наибольшим общим делителем. • Также существует избыточность в отношении между НОД и наименьшим общим кратным: для целых awb есть важное равенство И НОК(я,6)= LJ НОД(аМ которое тоже может быть легко проверено. Дополнительные полезные формулы, которые относятся к НОД и НОК, представлены в разделе 10.1.
390 Криптография на Си и C++ в действии • Наконец, для проверки теста на простоту, можно вызвать процедуру RSA: если р или q не простые, то ф(л) Ф (р - \){q - 1). Функции RSA будут работать корректно только если тест Ферма выдаст, что р и q вероятно простые. Поэтому некоторые взаимно обратные операции RSA и сравнение расшифрованного текста с первоначальным определенно выявят, что тест на простоту был реализован некорректно. Таким образом, существует достаточно много различных подходов к эффективному тестированию функций LINT. Читателю имее смысл разработать по крайней мере один подобный тест для функции LINT. Это будет очень полезно как для тестирования, так и в качестве примера работы с пользовательским классом LINT, чтобы понять, как он устроен и как его можно применять.
ГЛАВА 18. Направления дальнейших исследований Теперь, когда в нашем распоряжении уже есть пакет программ, реализующий строго сформулированные и протестированные функции, перед нами встает вопрос: в каком направлении двигаться дальше? Здесь могут быть два пути: расширение функциональности и повышение производительности. Что касается функциональности, читатель может попробовать применить основные функции пакета FLINT/C в тех областях, которые нами были лишь слегка затронуты или не рассматривались вообще, например, для разложения числа на множители или для арифметики эллиптических кривых. Последние, благодаря своим свойствам, находят все большее применение в криптографии. Заинтересованный читатель найдет подробное обсуждение этих тем в работах [Bres], [Kobl] и [Mene], а также в неоднократно нами упоминавшихся и содержащих многочисленные ссылки на литературу [Cohe], [Schn] и [MOV]. 1 Второе направление работ - искать пути повышения производительности. Прежде всего, увеличить размер основания системы счисления с 16 до 32 бит (В = 232), а также использовать ассемблерные функции и (на тех платформах, где это возможно) включить их в текст на языке C/C++. Разработка и тестирование, связанные со вторым направлением, * могут выполняться независимо от платформы, например, с ис- kI *' пользованием GNU-компилятора дсс с типом unsigned long long: :1 тип CLINT будет тогда определяться как typedef ULONG CLINT[CLINTMAXLONG];. Кроме того, нужно будет скорректировать .(,,., некоторые константы, связанные с системой счисления внутреннего представления целых чисел. В функциях пакета FLINT/C все явные приведения типов и прочие г" ссылки на тип USHORT следует заменить на ULONG, a ULONG, в свою очередь, на unsigned long long (или, после соответствующего typedef, на, скажем, ULLONG). Отдельные функции, зависящие от длины разряда в представлении данных, придется адаптировать. >v. После тщательного тестирования и отладки, включая статическую проверку синтаксиса (см. главу 12), пакет FLINT/C будет годиться и для 64-разрядных процессоров. Применение ассемблерных функций позволяет также работать с 32-битной длиной разряда и 64-битным результатом, это можно сделать, если процессор является 32-разрядным, но тем нее менее допускает 64-битный результат арифметических операций. Используя ассемблерные функции, мы отказываемся от ранее принятой стратегии независимости от конкретной платформы, а значит, хорошо бы реализовать эти функции как узко специализированные. Следовательно, нужно выбрать из пакета FLINT/C те функции, для
392 Криптография на Си и C++ в действии которых выигрыш от использования Ассемблера будет максимальным. Определить их нетрудно. Это функции, имеющие квад^ ратичную сложность: умножение, возведение в квадрат и деление. Поскольку базовые операции занимают большую часть времени выполнения теоретико-числовых функций, улучшение должно быть линейным, без непосредственного изменения алгоритмов. В пакете FLINT/C мы воспользовались этим «запасом прочности», реализовав на 80x86 Ассемблере функции mult(), umul(), sqr(), div_l(). Функции mult(), umul() и sqr() являются ядром для построения функций multJO, umulJO и sqr_l() соответственно (см. стр. 86). Эти функции допускают аргумент длиной до 4096 двоичных разрядов, то есть 256 (= МАХд) разрядов числа типа CLINT, и результат двойной длины. Функции на Ассемблере, как и соответствующие функции на С, реализованы в соответствии с алгоритмами главы 4, при этом регистр процессора позволяет обрабатывать арифметическими машинными командами 32-разрядные аргументы и 64-разрядный результат (см. главу 2). Ассемблерные модули mult.asm, umul.asm, sqr.asm и div.asm включены в пакет FLINT/C в виде исходных текстов. Их можно ассемблировать с помощью Microsoft MASM (вызов программы: ml /Сх/с /Gd <fjlename>) или Watcom WASM1, после чего заменить ими соответствующие функции на С, транслируя модуль flint.c с помощью -DFLINT_ASM2. Время вычислений, приведенное в приложении D, позволяет непосредственно сравнить реализацию некоторых важных функций с использованием Ассемблера и без него. Возведение в степень по Монтгомери (см. главу 6) дает дополнительную экономию времени. Кроме того, можно реализовать на 32- разрядном Ассемблере две вспомогательные функции mulmon_l() и sqrmonJO (см. стр. 128). Отправной точкой для такой реализации могут послужить модули mul.asm и sqr.asm. Для заинтересованного читателя здесь широчайшее поле деятельности. И это все, что мы знаем. Джон Хайзмен, Колизе' В зависимости от того, какой компилятор используется, идентификаторы mult, umul, sqr и div__ ассемблерных процедур следует снабдить символом подчеркивания (_mult, _umul, _sqr и _div_l) поскольку WASM его не генерирует. Модули mult.asm, sqr.asm, umul.asm и div.asm работают на 80х86-совместимых платформах. Дл других платформ необходима соответствующая программная реализация.
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных I Не знаю, есть ли у нас хоть малейший шанс. I Он умеет умножать, а мы можем лишь склады- I вать. Он - само воплощение прогресса, а я едва 1 -Hv". волочу ноги. 1 ,4t. Стэн Надольный, Бог дерзости I В 1997 году Американский Национальный институт стандартов и I технологий (National Institute of Standards and Technology, NIST) I объявил конкурс на разработку нового национального стандарта I (федерального стандарта обработки информации, FIPS) симмет- I ричного шифрования - улучшенного стандарта шифрования AES ■ (Advanced Encryption Standard). Хотя эта книга посвящена в основ- I ном асимметричной криптографии, этот стандарт настолько важен, I что мы уделим ему немного внимания (хотя бы из любопытства). I Для нового стандарта нужен был алгоритм шифрования, который I удовлетворял бы всем современным требованиям по безопасности I и с учетом всех аспектов построения и реализации мог бы свободно ■ распространяться по всему миру. И наконец, новый стандарт дол- I жен был прийти на смену действующему стандарту шифрования щ данных DES (Data Encryption Standard), который, правда, в виде В тройного DES (triple DES) по-прежнему будет использоваться в щ правительственных учреждениях. В дальнейшем предполагается I использовать AES в качестве основного криптографического I средства защиты уязвимых данных в административных учреж- I дениях США. I Конкурс на AES привлек всеобщее внимание как в самих Соеди- I ненных Штатах, так и за их пределами не только из-за того, что I любое событие в американской криптографии находит отклик по I всему миру, но потому, что участие зарубежных конкурсантов в ■ / разработке новой процедуры блочного шифрования всячески по- ■ ощрялось. I Из пятнадцати кандидатов, вступивших в борьбу в 1998 году, в В 1999 году международной группой экспертов десять было отверг- ■ нуто. А вот кто остался: алгоритм MARS фирмы IBM; RC6 от RSA В Laboratories; Rijndael Йоана Дамана (Joan Daemen) и Винсента Рай- В мана (Vincent Rijmen); Serpent Росса Андерсона (Ross Anderson), В дш/ Эли Бихама (Eli Biham) и Ларса Кнудсена (Lars Knudsen); Twofish В (nvs Брюса Шнайера (Bruce Schneier) и др. Наконец, в октябре 2000 года В был объявлен победитель. Алгоритм Rijndael Йоана Дамана и В Винсента Раймана из Бельгии был назван будущим улучшенным I
394 Криптография на Си и C++ в действии стандартом шифрования (см. [NIST]). l Rijndael является наследником блочного шифра «Square» («Квадрат»), опубликованного ранее этими же авторами (см. [Squa]), но оказавшегося не таким стойким. При разработке алгоритма Rijndael особое внимание обращалось на устранение слабостей Square. Институт NIST приводит следующие доводы в пользу алгоритма Rijndael. 1. Безопасность Все претенденты удовлетворяли требованиям по стойкости относительно известных атак. На фоне других алгоритмов Serpent и Rijndael смогли с меньшими потерями противостоять таким атакам, при которых информация извлекается из результатов измерения времени работы аппаратуры (так называемые временные (timing) атаки) и токовых импульсов (простые или дифференциальные токовые атаки (power attacks)) 2. Снижение производительности, вызванное наличием средств защиты от таких атак, меньше всего у Rijndael, Serpent и Twofish, причем алгоритм Rijndael в этом плане значительно лучше двух других. 2. Скорость Rijndael зашифровывает и расшифровывает быстрее всех. Этот алгоритм отличается хорошей производительностью на любой платформе: 32-разрядном процессоре, 8-разрядном микроконтроллере, смарт-карте, а также при аппаратной реализации (см. ниже). Rijndael быстрее всех вычисляет ключи раундов. 3. Затраты памяти Rijndael требует небольших затрат памяти ОЗУ и ПЗУ и поэтому лучше всего подходит для приложений с ограниченными ресурсами. В частности, ключи раундов можно вычислять «на лету». Эти свойства особенно важны для реализации на микроконтроллерах, используемых, например, в смарт-картах. Структура алгоритма такова, что в случаях, когда требуется только зашифрование или только расшифрование, требования к памяти ПЗУ минимальны и возрастают лишь при двунаправленном процессе. Тем не менее, по сравнению с другими четырьмя претендентами, по затратам ресурсов Rijndael вне конкуренции. Слово-гибрид «Rijndael» составлено из начальных слогов фамилий авторов. Мне говорили, чт правильное его произношение - это что-то среднее между «rain doll» и «Rhine dahl» (Райндол| Возможно, NIST все же включит в стандарт международную фонетическую транскрипцию этог слова. Токовые атаки основаны на зависимости потребляемой электроэнергии, затрачиваемой на выпол" нение отдельных команд или последовательностей команд, от отдельных битов или групп бито| секретного криптографического ключа (см., например, [КоЛ], [CJRR], [GoPa]).
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 39! 4. Аппаратная реализация Алгоритмы Rijndael и Serpent показали наилучшую производительность в части аппаратной реализации; Rijndael был немного лучше в режимах обратной связи по выходу и по шифртексту. NIST предложил следующие критерии отбора в пользу Rijndael (см. [NIST], п. 7): Никто не знает, в какой среде и на какой вычислительной платформе будет функционировать AES. Все качества алгоритма Rijndael: безопасность, производительность, гибкость и простота реализации - говорят о том, что этот алгоритм стоит использовать и сегодня, и в будущем. Прозрачный процесс выбора алгоритма и политический интерес к Rijndael как алгоритму «европейского происхождения» позволяют предположить, что не за горами всевозможные пересуды о скрытых свойствах, потайных лазейках и умышленных встроенных уязвимо- стях, которые преследовали (впрочем, без особого успеха) и алгоритм DES. Прежде чем заняться изучением работы алгоритма Rijndael, проведем небольшой экскурс в арифметику полиномов над конечными полями. Материал следующего параграфа в основном соответствует работе [DaRi], п. 2. 19.1. Полиномиальная арифметика 1 Начнем с изучения арифметических операций в поле F п - конеч- ^ ном поле из 2п элементов. Любой элемент поля F п можно представить в виде полинома J{x) = ап.{хп~1 + ап_2хп~2 + ...+ ахх + а0 с коэффициентами at из поля F2 (изоморфного полю Z2). Можно задать каждый элемент и просто набором из п коэффициентов полинома. О: У каждого из этих представлений есть свои преимущества. Пред- >i ставление в виде полинома удобнее для обработки вручную, тогда как представление набором коэффициентов хорошо «ложится» на двоичное представление чисел в компьютере. В качестве примера рассмотрим два представления поля F ъ: в виде последовательности из восьми полиномов и в виде восьми троек с соответствующими численными значениями (см. таблицу 19.1).
396 Криптография на Си и C++ в действии Таблица 79.1. Элементы поля F23 Полиномы в F з 23 0 1 X х+1 х2 х* + 1 х2 + х х2 +х+ 1 Тройки в F з 0 0 0 0 1 1 1 1 0 0 1 1 0 0 1 1 0 1 0 1 0 1 0 1 Численное значение '00' '01' '02' '03' '04' '05' '06' '07' Сложению полиномов соответствует сложение коэффициентов в F2: если fix) := х2 + х и g(x) := х2 + х + 1, то j{x) + g(x) = 2х2 + 2х + 1 = 1, так как в поле F2 выполняется равенство 1 + 1=0. Сложение троек в поле F з выполняется отдельно для каждого столбца. Например, сумма (1 1 0) и (1 1 1) равна (0 0 1): 1 1 0 0 111 0 0 1 Сложение разрядов выполняется в кольце Жъ его не следует путать с двоичным сложением, при котором могут возникать переносы. Оно напоминает нам функцию XOR из п. 7.2, которая выполняет ту же операцию в кольце Z„ для больших п. Чтобы перемножить два полинома в поле F 3, каждое слагаемое первого полинома нужно умножить на каждое слагаемое второго полинома, затем сложить частичные произведения и найти остаток от деления полученной суммы на неприводимый полином степени 3 (в нашем примере модуль т(х) := хъ + д: + 1):3 Ах) • g(x) = (х + х) • (х + х + 1) mod (х + х + 1) = = х4 + 2х3 + 2х2 + х mod (jc3 + x + 1) = = jc4 + х mod (jc3 + х + 1) = = jc2+ 1. Полином называется неприводимым, если он делится (без остатка) только на самого себя и на 1 •
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 397 Это соответствует произведению троек (1 10) • (1 1 1) = (101) или, в шестнадцатиричном виде, '06' • '07' = '05\ Элементы множества F 3 образуют абелеву группу относительно операции сложения, а элементы множества F 3 \ {0} - относительно операции умножения (см. главу 5). Выполняется и закон дистрибутивности. Структуру и арифметику поля F 3 можно перенести и на поле F g - -fi. именно оно нам понадобится при изучении алгоритма Rijndael. п Сложение и умножение выполняются точно так же, как в примере м, выше, с тем лишь отличием, что в поле F 8 уже 256 элементов, а в качестве модуля нужно взять неприводимый полином степени 8. В Rijndael это полином т(х) := х* + х4 + х3 + х + 1, соответствующий *7 набор коэффициентов (100011011), а шестнадцатиричное число- '011В\ Умножение полинома f[x) = a-jx1 + tffc*6 + а$х5 + а^х4 + а^хъ + а2х2 + а\Х + а0 с на х (что соответствует умножению • '02') выполняется особенно просто: j{x) • х = а7хъ + Яб*1 + Д5*6 + Яф*5 + аз** + сцхЪ + ci\X2 + а&с mod m(x), i* где приведение по модулю т(х) требуется лишь в случае a-j^O, и для этого нужно просто вычесть ш(х), то есть вычислить сумму . коэффициентов по модулю 2 операцией XOR. t При программировании коэффициенты полиномов следует рас- ' сматривать как двоичные разряды целого числа. Умножение на х осуществляется как сдвиг влево на один бит, после чего (в случае * • а7 Ф 0) результат суммируется по модулю 2 с восемью младшими г. битами '1В' числа '011В', соответствующего модулю т(х) (при этом коэффициент а7 просто «забывается»). Операцию а • '02' для 1 полинома /, где а - это его численное значение, Даман и Райман *: обозначили через b = xtime(a). Умножение на степени jc выполняется путем последовательного выполнения операции xtime. "" Например, умножение полиномаДх) нах+ 1 (или на '03') выполняется как сдвиг двоичных разрядов числа а, соответствующего полиному/, на одну позицию влево и сложение по модулю 2 (XOR) результата с а. Приведение по модулю т(х) выполняется так же, как и для функции xtime. Процедуре соответствуют две строчки на языке С: f л= f « -J. /* умножение f на (х + 1) 7 if (f & 0x100) f л= 0x11 В; /* Приведение по модулю т(х) 7
398 Криптография на Си и C++ в действии Умножение двух полиномов / и g в F 8 \ {0} можно ускорить, используя дискретные логарифмы. Пусть полином g(x) является образующей4 группы F8\{0}. Тогда существуют числа тип такие, что/= gm и h = gn, то есть/- h = gm+n mod m(x). В переводе на язык программирования это означает, что можно составить две таблицы, в одну из которых мы записываем 255 степеней образующей g(x) := х + 1, а в другую - логарифмы по основанию g(x) (см. таблицы 19.2 и 19.3). Теперь, чтобы вычислить произведение/- /г, нужно обратиться к этим таблицам три раза: из таблицы логарифмов берем значения тип, для которых gm =/и gn -h. По таблице степеней находим значение g«n+w)mod 255> (заметим, чтояогад=1). 1 95 229 83 76 131 181 254 251 195 159 155 252 69 18 57 3 225 52 245 212 158 196 25 22 94 186 182 31 207 54 75 5 56 92 4 103 185 87 43 58 226 213 193 33 74 90 221 15 72 228 12 169 208 249 125 78 61 100 88 99 222 238 124 17 216 55 20 224 107 16 135 210 71 172 232 165 121 41 132 51 115 89 60 59 189 48 146 109 201 239 35 244 139 123 151 85 149 235 68 77 220 80 173 183 64 42 101 7 134 141 162 255 164 38 204 215 127 240 236 194 192 126 175 9 145 140 253 26 247 106 79 98 129 11 47 93 91 130 234 27 168 143 28 46 2 190 209 166 152 29 113 231 237 157 37 45 227 138 36 114 6 217 104 241 179 39 147 50 44 188 111 119 62 133 108 150 10 112 184 8 206 105 174 86 116 223 177 153 66 148 180 161 30 144 211 24 73 187 233 250 156 122 200 176 198 167 199 248 34 171 110 40 219 214 32 21 191 142 67 203 81 242 82 19 102 230 178 120 118 97 96 63 218 137 197 70 243 13 246 53 170 49 205 136 154 163 160 65 117 128 84 202 14 23 1 Таблица 19.2. Степени полинома g(x) = х + 7 g является образующей группы F 8\ {0}, если порядок элемента g равен 255. То есть степени элемента g пробегают всю группу F 8 \ {0}.
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 399 О 25 100 4 224 125 194 29 101 47 138 150 143 219 102 221 253 126 110 72 43 121 10 175 88 168 44 215 117 127 12 246 204 187 62 151 178 135 83 57 132 68 17 146 103 74 237 1 50 14 52 181 249 5 33 189 54 48 191 195 163 21 155 80 244 122 235 111 23 90 251 144 97 60 65 217 35 222 197 2 26 141 129 185 39 15 225 208 206 6 139 182 30 159 94 234 214 22 11 196 73 96 177 190 220 162 109 32 46 49 254 198 75 239 76 106 77 36 18 148 19 98 179 66 58 202 78 116 79 245 89 236 216 134 59 252 188 71 20 137 180 24 13 199 27 104 113 8 200 228 166 114 240 130 69 92 210 241 37 226 152 107 40 84 212 172 229 174 233 213 203 95 176 67 31 45 82 161 108 149 207 205 42 158 93 124 184 38 99 140 128 51 238 248 105 154 201 53 147 64 70 34 136 250 133 243 115 231 230 156 169 164 118 170 85 55 63 86 242 119 153 192 247 223 3 28 193 9 120 218 142 131 56 Н5 16 61 186 167 87 1?3 232 81 160 123 183 41 157 91 209 211 171 227 165 112 7 Таблииа 193. Логарифмы по основанию g(x) = х+ 1 (например, logg(x)2 = 25, loggM255 = 7) » > Mi- С помощью этого же механизма можно выполнять и деление полиномов в F 8 \ {0}. А именно: £ = А1-]=8т(8"Г . и-д _ (m-n)mod255 Теперь поднимемся еще на одну ступеньку сложности и рассмотрим арифметику полиномов вида J{x) =fyc3 +/2*2+Л*+/о с коэффициентами fi из поля F 8, то есть сами коэффициенты - это тоже полиномы. Каждый такой коэффициент можно представить в виде четырехбайтового поля. Теперь еще интересней: сложение полиномов j{x) и g(x) по-прежнему выполняется путем побитового сложения коэффициентов по модулю 2, а вот произведение h(x) -f(x)g(x) вычисляется в виде h(x) = h&x + h$x + htfc + h^x + h2x + h{x + /z0,
400 Криптография на Си и C++ в действии где коэффициенты hk '= У. ._0fj • gj > знак суммы означает сложение 0 в поле Fg. Приводим полином h(x) по модулю полинома степени 4 и опять к получаем полином степени 3 над полем F 8. В качестве такого полинома в Rijndael взят полином М(х) := х4 + 1. 1i Поскольку х! mod М{х) = У mod 4, вычет h(x) mod M(x) можно вычислить как d(x) :=ЯХ) ® 8(х) '•= h(x) mod M(x) = d^x3 + d2x2 + dxx + d0y где do = a0 • b0 0 a3 • b\ 0 «2 • ^2 Ф я i • ^з> Jj = Л1 • /?0 0 a0 • b{ 0 a3 • /?2 Ф «2 • ^з, ^2 = ^2 • b0 © «i • b\ 0 a0 • ^2 ® #з • ^з» d3 = a3 • b0 0 a2 • &i 0 «i • b2 0 «o • ^з- Отсюда видно, что коэффициенты dt являются результатом умн жения на матрицу в поле F 8: d0 Ч d2 kj а0 аъ а2 а, а, а0 аъ а2 аг а, а0 аъ _аъ а2 а, а0_ м £, £2 1а J Именно эта операция с фиксированным, обратимым по модул М(х) полиномом а(х):= а^х3 + а2х2 + а\х + ао над полем F 8, гд а0(х) = х, а\(х) = 1, я2М = 1 и язМ = * + 1, выполняется в так назы ваемом преобразовании MixColumn - одном из основных компонен тов преобразования раунда в Rijndael. 19.2. Алгоритм Rijndael Rijndael - это симметричный блочный алгоритм шифрования с переменной длиной блока и ключа. Длины блока и ключа могут принимать значения 128, 192 и 256, причем в любой комбинации Варьируемое значение длины ключа составляет одно из достоинст стандарта AES, а вот «официальная» длина блока - только 128 бит
19. Rijndael: наследник стандарта шифрования данных 40 Таблица 19 А, Число раунлов в алгоритме Rijndael как функция от ллины блока и ключа Каждый блок открытого текста зашифровывается несколько раз i так называемых раундах (round) с помощью повторяющейся после довательности различных функций. Число раундов зависит от длинь блока и ключа (см. таблицу 19.4). Алина ключа (в битах) 128 192 256 Алина блока (в битах) 128 10 12 14 192 12 12 14 256 14 14 14 Rijndael не относится к алгоритмам на сетях Файстеля, которые характеризуются тем, что блок текста разбивается на левую и правую половины, затем преобразование раунда применяется к одной половине, результат складывается по модулю 2 с другой половиной, после чего эти половины меняются местами. Самым известным блочным алгоритмом из этой серии является DES. Rijndael, напротив, состоит из отдельных уровней, каждый из которых по-своему воздействует на блок в целом. Для зашифрования блока последовательно выполняются следующие преобразования: Первый раундовый ключ складывается с блоком по модулю 2 (XOR). Выполняются Lr - 1 обычных раундов. 3. Выполняется завершающий раунд, в котором, в отличие от обычного, отсутствует преобразование MixColumn. Каждый обычный раунд на этапе 2 состоит из четырех отдельных шагов, которые мы сейчас и изучим: 1. Подстановка. Каждый байт блока заменяется значением, которое определяется 5-блоком. 2. Перестановка. Байты в блоке переставляются с помощью преобразования ShiftRow. 3. Перемешивание. Выполняется преобразование MixColumn. 4. Сложение с раундовым ключом. Текущий раундовый ключ складывается с блоком по модулю 2.
402 Криптография на Си и C++ в действии Подстановка (S-блок) ShiftRow MixColumn Сложение с раундовым ключом Уровневые преобразования внутри одного раунда схематично изображены на рис. 19.1. -г, ■*.-«: чглттш*1? ' ТЖР* ..;Жтг^* I I l l \, X X X l J I X l X X .1 S S S S S S S S S S S S S S S S Ф-^4-Ш4^ А-Ш "^М<У \ТГМ\ 4-I^J4J4J Рисунок 19.1. Уровни преобразования внутри олного раунла алгоритма Rijndael Каждый уровень оказывает на каждый из блоков открытого текста определенное воздействие. 1. Влияние ключа Сложение текста с ключом до первого раунда и на последнем шаге внутри каждого раунда влияет на каждый бит результата раунда. В процессе зашифрования результат каждого шага в каждом бите зависит от ключа. 2. Нелинейный уровень Операция подстановки в S-блоке является нелинейной. Строение S-блоков обеспечивает почти идеальную защиту от дифференциального и линейного криптоанализа (см. [BiSh] и [NIST]). 3. Линейный уровень Преобразования ShiftRow и MixColumn обеспечивают максимальное перемешивание битов в блоке. Далее в описании внутренних функций алгоритма Rijndael, через Lb будем обозначать длину блока в четырехбайтовых словах, через Lk - длину ключа пользователя в четырехбайтовых словах (то есть Lb, Lk е {4, 6, 8}) и через Lr - число раундов (см. таблицу 19.4). Открытый и зашифрованный тексты представлены в виде полей байтов и являются соответственно входом и выходом алгоритма.
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 403 Блок открытого текста, обрабатываемый как поле ш0, ..., m*L-\, представлен в виде двумерной структуры В (см. таблицу 19.5), в которой байты открытого текста отсортированы в следующем порядке: гщ -> Z?o,(b >"i -> ^i,o» "h -» ^2,о» '"з -> £3,0, т -> ^о,ь Щ -^> Ьии ..., т.е. m„ -> fe,j, где i = n mod 4 и j = |_n/4j. Таблииа 19.5. Прелставление блоков открытого текста Ь0/о Ь),о Ь2,о Ь3/о b0/i Ь2/1 Ьз,1 Ь0/2 ЬЬ2 Ь2,2 Ь3/2 Ь0/з ^2,3 Ьз,з Ь0/4 Ь2,4 Ь3,4 • ьиьл • ^ • Ьз^ь-1 Доступ к структуре 8 в функциях алгоритма Rijndael осуществляется по-разному, в зависимости от операции. S-блок оперирует с битами, ShiftRow - со строками (6I>0, 6U, bul, ..., buL1) структуры В, а функции AddRoundKey и MixColumn - с четырехбайтовыми словами, обращаясь к столбцам (bQj, b\jy b2j> ^з^)- 19.3. Вычисление ключа раунда И для зашифрования, и для расшифрования требуется сгенерировать Lr раундовых ключей, совокупность которых называется разверткой ключа {key schedule). Развертка строится путем присоединения к секретному ключу пользователя рекурсивно получаемых четырехбайтовых слов k\ = (k0j, ku, к2,ь &з,<)- Первые Lk слов к0, ..., kL { развертки ключа - это сам секретный ключ пользователя. Для Lke {4, 6} очередное четырехбайтовое слово ki определяется как сумма по модулю 2 предыдущего слова &м со словом ki-L. При i = 0 mod Lk перед операцией XOR нужно применить функцию FL(k, i), которая включает в себя циклический сдвиг к байтов влево (операция г(к)), подстановку S(r(k)) с использованием S-блока алгоритма Rijndael (к этой операции мы еще вернемся) и сложение по модулю 2 с константой с (b'AL*J)- Итоговое уравнение функции F таково: FL(k, i) := S(r(k)) ® c(li/Lkj). Константы c(j) задаются равенством c(j) := (rc(/), 0, 0, 0), где значения гс(/) определяются рекурсивно как элементы поля F^: гс(1) := 1, гс(/) :=гс(/- 1) -х = х?~1. Или в виде численных значений: гс(1) := «0Г, гс(/) :=гс(/- 1) • '02 \ Программно значение гс(/)
404 Криптография на Си и C++ в действии Таблица 19.6. Константы rc(j) (в шестналиати- ричном виле) Таблииа 19.7. Константы rc(j) (в лвоичном виле) реализуется (/- 1)-кратным рекурсивным вызовом упоминавшейся выше функции xtime, с начальным значением аргумента, равным 1, или более быстро - с использованием таблицы предвычислений (см. таблицы 19.6 и 19.7). '01' '02' '04' '08' '10' '20' '40' '80' '1В' '36' '6С 'D8' 'АВ' '4D' '9А' '2F' '5Е' 'ВС '63' 'С6' '97' '35' '6А' 'D4' 'ВЗ' 7D' TA; TF' 'С5 '91' 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000 00011011 00110110 01101100 11011000 10101011 01001101 10011010 00101111 01011110 10111100 01100011 11000110 10010111 00110101 01101010 11010100 10110011 01111101 11111010 11101111 11000101 10010001 Для ключей длины 256 бит (то есть при Lk = 8) введена дополнительная операция подстановки: при i = 4modLk перед операцией XOR значение fcM заменяется на S(£;_i). Таким образом, развертка ключей состоит из Lb- (Lr+ 1) четырехбайтовых слов, включая и секретный ключ пользователя. На каждом раунде / = 0, ..., Lr- 1 очередные Lb четырехбайтовых слова с кц по &l.-(/+i) выбираются из развертки и используются в качестве ключа раунда. Раундовые ключи рассматриваются, по аналогии с блоками открытого текста, как двумерная структура (см. таблицу 19.8). Таблииа 19.8. Прелставление раунловых ключей ^0,0 ^1,0 ^2,0 ^3,0 ^0,1 Ку ^2,1 *3.1 ^0,2 к\,г ki,2 *3,2 ^0,3 \ъ Къ Къ *0,4 к]Л к2,4 кз,л к°Н-] *ub-] k2,Lb-l k3,Lb-l
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 405 Для ключей длины 128 бит процесс генерации ключа изображен на рис. 19.2. Секретный ключ пользователя 'Лй^вяадмг * h ■4 4 4 h 4 4 Ф h $ > —' 4 4 4 4 Ф Рисунок 19.2. Анаграмма раунловых ключей аля Lk = 4 Пока не известны слабые ключи, использование которых неблагоприятно сказалось бы на стойкости алгоритма Rijndael. 19.4. S-блок Блок подстановки, или S-блок алгоритма Rijndael показывает, каким значением следует заменять каждый байт блока текста на каждом раунде. S-блок представляет собой список из 256 байтов. Сначала каждый ненулевой байт рассматривается как элемент поля F g и заменяется мультипликативно обратным (нулевые байты остаются неизменными). Затем выполняется следующее аффинное преобразование над полем F2 путем умножения на матрицу и сложения с вектором (1 1000 110): Уо Ух У2 Уз Уа Уз Уь Уп 10 0 0 1111 110 0 0 111 1110 0 0 11 11110 0 0 1 111110 0 0 0 111110 0 0 0 111110 0 0 0 11111 *0 *\ х2 ХЪ х4 *5 *6 *7 + 1 1 0 0 0 1 1 0
106 Криптография на Си и C++ в действии Здесь через х0 и у0 обозначены младшие, а через х7 и у7 - старшие биты в байте; вектор (1 10001 10) длины 8 соответствует шестнадцатиричному числу '63'. S-блок построен так, чтобы свести к минимуму чувствительность алгоритма к дифференциальному и линейному методам криптоанализа, а также к алгебраическим атакам. Последовательно применяя приведенную выше процедуру к числам от 0 до 255, получаем таблицу 19.9 (значения идут по строкам слева направо). 99 202 183 4 9 83 208 81 205 96 224 231 186 112 225 140 124 130 253 199 131 209 239 163 12 129 50 200 120 62 248 161 119 201 147 35 44 0 170 64 19 79 58 55 37 181 152 137 123 125 38 195 26 237 251 143 236 220 10 109 46 102 17 13 242 250 54 24 27 32 67 146 95 34 73 141 28 72 105 191 107 89 63 150 110 252 77 157 151 42 6 213 166 3 217 230 111 71 247 5 90 177 51 56 68 144 36 78 180 246 142 66 197 240 204 154 160 91 133 245 23 136 92 169 198 14 148 104 48 173 52 7 82 106 69 188 196 70 194 108 232 97 155 65 1 212 165 18 59 203 249 182 167 238 211 86 221 53 30 153 103 162 229 128 214 190 2 218 126 184 172 244 116 87 135 45 43 175 241 226 179 57 127 33 61 20 98 234 31 185 233 15 254 156 113 235 41 74 80 16 100 222 145 101 75 134 206 176 215 164 216 39 227 76 60 255 93 94 149 122 189 193 85 84 171 114 49 178 47 88 159 243 25 11 228 174 139 29 40 187 118 192 21 117 132 207 168 210 115 219 121 8 138 158 223 22 Таблица 19.9. Значения S-блока При расшифровании порядок действий меняется на противоположный. Сначала выполняется обратное аффинное преобразование, затем мультипликативное обращение в поле F 8. Обратный S-блок приведен в таблице 19.10.
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 407 Таблица 19,10. Значения обратного S-блока 82 124 84 8 114 108 144 208 58 150 71 252 31 96 160 23 9 227 123 46 248 112 216 44 145 172 241 86 221 81 224 43 106 57 148 161 246 72 171 30 17 116 26 62 168 127 59 4 213 130 50 102 100 80 0 143 65 34 113 75 51 169 77 126 48 155 166 40 134 253 140 202 79 231 29 198 136 25 174 186 54 47 194 217 104 237 188 63 103 173 41 210 7 181 42 119 165 255 35 36 152 185 211 15 220 53 197 121 199 74 245 214 56 135 61 178 22 218 10 2 234 133 137 32 49 13 176 38 191 52 238 118 212 94 247 193 151 226 111 154 177 45 200 225 64 142 76 91 164 21 228 175 242 249 183 219 18 229 235 105 163 67 149 162 92 70 88 189 207 55 98 192 16 122 187 20 158 68 11 73 204 87 5 3 206 232 14 254 89 159 60 99 129 196 66 109 93 167 184 1 240 28 170 120 39 147 131 85 243 222 250 139 101 141 179 19 180 117 24 205 128 201 83 33 215 233 195 209 182 157 69 138 230 223 190 90 236 156 153 12 251 203 78 37 146 132 6 107 115 110 27 244 95 239 97 125 19.5. Преобразование ShiftRow Следующий шаг раунда - перестановка байтов в блоке. Порядок байтов меняется в строке (bL0, Ь(Л, biy2, ..., ^/,l,-i) структуры В в соответствии с таблицами 19.11-19.13. Таблица 19,11. On ера иия ShiftRow аля блоков ллины 128бит0.ь = 4) До операции ShiftRow 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 После операции ShiftRow 0 4 8 12 5 9 13 1 10 14 2 6 15 3 7 11
408 Криптография на Си и C++ в действии Таблица 19.12. Операция ShiftRow лля блоков ллины 192 бита (Lb = 6) До операции ShiftRow 0 4 8 12 16 20 1 5 9 13 17 21 2 6 10 14 18 22 3 7 11 15 19 23 После операции ShiftRow 0 4 8 12 16 20 5 9 13 17 21 1 10 14 18 22 2 6 15 19 23 3 7 11 Таблииа 19.13. Операиия ShiftRow аля блоков ллины 256 бит (Lb = 8) До операции ShiftRow После операции ShiftRow 0 4 8 12 16 20 24 28 1 5 9 13 17 21 25 29 2 6 10 14 18 22 26 30 3 7 11 15 19 23 27 31 0 4 8 5 9 13 14 18 22 19 23 27 12 16 20 24 28 17 21 25 29 1 26 30 2 6 10 31 3 7 11 15 Все нулевые строки остаются без изменений. В строках /= 1, 2, 3 байты циклически сдвигаются влево на cL .■ позиций: с позиции с номером у на позицию с номером j - cL ,mod Lb, где значение cL^ определяется по таблице 19.14. Таблица 19.14. Размер сленга строк в операиии ShiftRow Lb 4 6 8 <V 1 1 1 cv 2 2 3 СЧ,<3 3 3 4 При обратном преобразовании позиция с номером j в строках / = 1, 2, 3 сдвигается на позицию с номером j + cL .• mod Lb. 19.6. Преобразование MixColumn После того как выполнена последняя построчная перестановка, на следующем шаге каждый столбец (bjj) блока текста, где / = 0, ..., 3, у = 0, ..., Lb, представляется в виде полинома над полем F28 и умножается на фиксированный полином а(х) := аухъ + а2х2 + а\Х + До с коэффициентами а0(х) =х, ах(х) = 1, а2(х) = 1, а3(х) =х+ 1. Затем
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 409 вычисляется остаток от деления полученного произведения на модуль М(х) :=*4+ 1. Таким образом, каждый байт столбца взаимодействует со всеми остальными байтами столбца. При построковом преобразовании ShiftRow на каждом раунде байты взаимодействуют друг с другом в других комбинациях. То есть эти две операции дают сильное перемешивание. Мы уже видели (см. стр. 400), как этот шаг можно свести к умножению на матрицу: bQ,j *2J KJ <— '02' '03' ЮГ '0Г •0Г '02' '03' '0Г •0Г '0Г '02' '03' '031 '01' '0Г '02' Умножение на '02' (соответственно на х) мы уже реализовали в виде функции xtime. Умножение на '03' (соответственно на jc+ 1) тоже сделано по аналогии (см. стр. 397). Для обращения преобразования MixColumn умножаем каждый столбец (bij) блока текста на полином г(х) := г3х3 + г2х2 + гхх + г0 с коэффициентам и г0(х) = х3 + х2 + х, гх (х) = х3 + 1, г2(х) = х3 + х2 + 1, г3(х) =х3 + х+ 1 и приводим результат по модулю М(х) :=х4+ 1. Соответствующая матрица имеет вид: •0F '0В' Ш' '09'" •09' '0Е' '0В' '0D' '0D' '09' '0Е W •0В' Ш' '09' '0F 19.7. Сложение с ключом раунда На последнем шаге цикла раундовый ключ складывается по модулю 2 с блоком текста: (b0J, bu, b2J, b3J) <- (b0Jy bij, b2J, b3J) © (koj, kij, k2J, k3J), где; = 0, ...,Lb- 1. 19.8. Полная процедура зашифрования блока Зашифрование по алгоритму Rijndael реализуется в виде следующего псевдокода (см. [DaRi], пп. 4.2-4.3). Аргументы обрабатываются как указатели на поля байтов или четырехбайтовых слов. Интерпретация полей, переменных и функций дана в таблицах 19.15-19.17.
410 Криптография на Си и C++ в действии Таблииа 19.15. Интерпретаиия переменных Таблииа 19.16. Интерпретаиия полей Таблииа 19.17. Интерпретаиия функиий Переменные Nk Nb Nr Интерпретаиия Длина Ц секретного ключа пользователя в четырехбайтовых словах Длина Ц блока в четырехбайтовых словах Число раундов Lr в соответствии с таблицами выше Переменные CipherKey ExpandedKey Rcon State RoundKey Размер в байтах 4*Nk 4*Nb*(Nr+1) r4*Nb*(Nr+1)/Nkl 4*Nb 4*Nb Интерпретаиия Секретный ключ пользователя Поле четырехбайтовых слов под развертку ключа Поле четырехбайтовых слов под константы c(j) := (rc(j), 0, 0, 0) Поле ввода открытого текста и вывода зашифрованного текста Раундовый ключ, фрагмент ExpandedKey Функция KeyExpansion RotByte ByteSub Round FinalRound ShiftRow MixColumn AddRoundKey Интерпретаиия Генерация раундового ключа Сдвиг четырехбайтового слова влево на 1 байт: (abed) -> (beda) Подстановка в S-блоке всех байтов поля шШ Обычный раунд JH Последний раунд без преобразования MixColumn Преобразование ShiftRow Преобразование MixColumn Сложение с ключом раунда Генерация ключа при Lk<8: KeyExpansion (byte CipherKey, word ExpandedKey) { for (i = 0; i < Nk; i++) ExpandedKey[i] = (CipherKey[4*i], CipherKey[4*i + 1], CipherKey[4*i + 2], CipherKey[4*i + 3]);
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 411 for (i = Nk; i < Nb * (Nr +1); { temp = ExpandedKey[j - 1]; jf (j % Nk == 0) temp = ByteSub (RotByte (temp))л Rcon[i/Nk]; ExpandedKey[i] = ExpandedKey[i - Nk] л temp; } } Генерация ключа при Lk - 8: KeyExpansion (byte CipherKey, word ExpandedKey) { for (i = 0; i < Nk; i++) ExpandedKeyfi] = (CipherKey[4*i], CipherKey[4*i + 1], CipherKey[4*i + 2], CipherKey[4*i + 3]); for (i = Nk; i < Nb * (Nr + 1); i++) { temp = ExpandedKey[i - 1]; if (i % Nk == 0) temp = ByteSub (RotByte (temp))Л Rcon[i/Nk]; else if (i % Nk == 4) temp = ByteSub (temp); ExpandedKeyfi] = ExpandedKeyfi - Nk]л temp; } } Раундовые функции: Round (word State, word RoundKey) { ByteSub (State); ShiftRow (State); MixColumn (State); AddRoundKey (State, RoundKey) }
412 Криптография на Си и C++ в действии FinalRound (word State, word RoundKey) { ByteSub (State); ShiftRow (State); AddRoundKey (State, RoundKey) } Полная процедура зашифрования блока текста: Rijndael (byte State, byte CipherKey) { KeyExpansion (CipherKey, ExpandedKey); AddRoundKey (State, ExpandedKey); for (j = 1; i < Nr; i++) Round (State, ExpandedKey + Nb*i); FinalRound (State, ExpandedKey + Nb*Nr); } Раундовый ключ можно заготовить и вне функции Rijndael, a вместо ключа пользователя CipherKey использовать развертку ключей ExpandedKey. Преимущества такого варианта очевидны, когда для зашифрования текста длиной более одного блока нужно несколько раз вызывать функцию Rijndael с одним и тем же ключом пользователя. Rijndael (byte State, byte ExpandedKey) { AddRoundKey (State, ExpandedKey); for (i = 1; i < Nr; i++) Round (State, ExpandedKey + Nb*i); FinalRound (State, ExpandedKey + Nb*Nr); } Для 32-разрядных процессоров раундовое преобразование лучше вычислять заранее и хранить результаты в виде таблиц. Заменяя операции перестановки и умножения на матрицу обращением к таблице, мы значительно сокращаем время работы как при зашифровании, так и (как будет видно позже) при расшифровании. С помощью таблиц вида
ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 413 ~S(w) • '02'~ S(w) _S(w) • '03'_ S(w) S(w) • '03' S(w) • '02' S(w) , ВД:= , T3[w]:= "5(w) • '03'] S(w) • '02' S(w) ' u S(w) J S(w) 1 5(w) S(w) • '03' _S(w) • '02'J каждая из которых содержит по 256 четырехбайтовых слов (здесь w = 0, ..., 255; S(w) - S-блок подстановки), преобразование блока b = (b0'j, bij, b2J, b3J)J = 0, ...,Lb- 1, можно быстро выполнить как (V blJy b2J, b3J) <- T0[b0J] e Tx[blAlJ)] e т2[£2Д2х/)] © т3[б3дзл] © *> где d(i J) :=j + cLjyi mod Lb (см. ShiftRow, таблица 19.14) и ^/= (^<v> ^ij» &2j> ^3j) -у-й столбец раундового ключа. Вывод этого результата см. в [DaRi], п. 5.2.1. На последнем раунде преобразование MixColumn не выполняется, поэтому результат получается как bj := (b0J, bij, b2J, b3J) <- (S(b0J), S(biAiJ)), S(b2A2J)), S(b3A3J))) 0 kj. Конечно, можно воспользоваться таблицей из 256 четырехбайтовых слов, тогда bj <- T0[b0J] 0 r(T0[b]AlJ)] 0 r(T0[b2A2J)] © r(T0[b3A3J)]))) 0 kJf где r{a, b,c,d) = (d, я, b, с) - циклический сдвиг вправо на один байт. Для приложений с ограниченной памятью этот вариант чрезвычайно удобен, немного увеличится лишь время вычислений, необходимое для выполнения трех сдвигов. 19.9. Расшифрование г При расшифровании алгоритмом Rijndael процесс зашифрования •'•' выполняется в обратном порядке с обратными преобразованиями. Мы уже рассматривали преобразования, обратные к ByteSub, ShiftRow и MixColumn; ниже они представлены в псевдокодах функциями InvByteSub, InvShiftRow и InvMixColumn. Обратный S-блок, размер сдвига для обращения преобразования ShiftRow и обратная матрица для обращения преобразования MixColumn приведены на стр. 407-408. Вот эти обратные функции: InvFinalRound (word State, word RoundKey) {
14 Криптография на Си и C++ в действии AddRoundKey (State, RoundKey); InvShiftRow (State); InvByteSub (State); } InvRound (word State, word RoundKey) { AddRoundKey (State, RoundKey); InvMixColumn (State); InvShiftRow (State); InvByteSub (State); } Полная процедура расшифрования выглядит следующим образом: InvRijndael (byte State, byte CipherKey) { KeyExpansion (CipherKey, ExpandedKey); InvFinalRound (State, ExpandedKey + Nb*Nr); for(i = Nr-1;i>0;H InvRound (State, ExpandedKey + Nb*i); AddRoundKey (State, ExpandedKey); } Алгебраическая структура алгоритма Rijndael позволяет упорядочить преобразования зашифрования так, что и для них можно будет использовать таблицы. Заметим, что подстановка S и преобразование ShiftRow коммутируют, поэтому внутри одного раунда их можно поменять местами. Благодаря свойству гомоморфизма j{x + у) =J{x) +fiy) линейных преобразований, операции InvMixColumn и сложение с раундовым ключом можно тоже поменять местами. В пределах одного раунда это выглядит так: InvFinalRound (word State, word RoundKey) { AddRoundKey (State, RoundKey); InvByteSub (State); InvShiftRow (State); }
ABA 19. Rijndael: наследник стандарта шифрования данных 415 InvRound (word State, word RoundKey) { InvMixColumn (State); AddRoundKey (State, InvMixColumn (RoundKey)); InvByteSub (State); InvShiftRow (State); } Если порядок функций не менять, то их можно переопределить: AddRoundKey (State, RoundKey); InvRound (word State, word RoundKey) { InvByteSub (State); InvShiftRow (State); InvMixColumn (State); AddRoundKey (State, InvMixColumn (RoundKey)); } InvFinalRound (word State, word RoundKey) { InvByteSub (State); InvShiftRow (State); AddRoundKey (State, RoundKey); } Отсюда получаем структуру, аналогичную зашифрованию. Из соображений эффективности в процедуре lnvRound() отложим применение функции InvMixColumn к раундовому ключу до вычисления развертки, в которой первый и последний раундовые ключи из InvMixColumn оставим без изменений. «Обратные» раундовые ключи генерируются процедурой InvKeyExpansion (byte CipherKey, word InvEpandedKey) { KeyExpansion (CipherKey, InvExpandedKey); for (i = 1; i < Nr; i++)
И 6 Криптография на Си и C++ в действии InvMixColumn (InvExpandedKey + Nb*i); } Теперь полная процедура расшифрования выглядит так: InvRijndael (byte State, byte CipherKey) { InvKeyExpansion (CipherKey, InvExpandedKey); AddRoundKey (State, InvExpandedKey + Nb*Nr); for(i = Nr-1;i>0;i») InvRound (State, InvExpandedKey + Nb*i); InvFinalRound (State, InvExpandedKey); } По аналогии с зашифрованием можно и для расшифрования составить таблицы предвычислений. С помощью •ГЧиО-'ОЕ' S-l(w)^W, S~l(w) • *0В' S^OvWOD'" S~V)*'0B' STV^'OE' ^(^•W1 . тг . т3- V]:= lM:= S-l(w)m'0B' S-l(w)^Ш S-l(w)^W^ _S~l (*>)•№ ~S~\w)oW S-\W)^^0B^ S"1(w)»,0E' (где w = 0, ..., 255; S~l(w) - обратный S-блок подстановки) получаем результат обратного раундового преобразования блока Ь = (Ч/, *ij> *2j, b3J)J = 0, ..., Lb- 1: bj<- To'l[boj] © т,-1^^] © т2-WW © т3-WW © *Л где d~l(ij) :=j-cL j mod Lb (см. стр. 408) и А:/1 -у-й столбец «обратного» раундового ключа. Здесь на последнем раунде тоже не выполняется преобразование MixColumn, и результатом последнего раунда будет bj <- (S-l(b0J), S-^bu-hij)), S" W W> S" WW) © kj~\ где./ = 0, ...,£*-1.
ABA 19. Rijndael: наследник стандарта шифрования данных 417 Для экономии памяти для расшифрования также можно составить таблицу всего из 256 четырехбайтовых слов, в которой bj <- T0-l[b0j] е r(T0-l[bur\^) ® кт0-WW е © кт0-WW))) ® *Л где r(a, b,c,d) = (d, а, Ъ, с) - циклический сдвиг вправо на один байт. Дальнейшие подробности описания, вопросов безопасности, результатов криптоанализа, вычислительных аспектов, а также текущую информацию об AES и Rijndael читатель найдет в источниках, указанных по тексту этой главы, а также на Интернет-сайте NIST и страничке Винсента Раймана, которые также содержат множество полезных ссылок: http://csrc.nist.gov/encryption/aes/aes_home.htm http://www.esat.kuleuven.ac.be/~rijmen/rijndael На прилагаемом к книге компакт-диске содержатся три реализации алгоритма Rijndael, рекомендуемые читателю для лучшего понимания процедуры и для дальнейших исследований. Программами предусмотрены функции шифрования в режимах обратной связи по шифртексту (Cipher Feedback Mode, CFB) и обратной связи по выходу (Output Feedback Mode, OFB). Автор хочет еще раз поблагодарить всех, кто предоставил исходные тексты программ, включенные в эту книгу. Исходные файлы расположены в следующих каталогах: Каталог rijndael\c_ref rijndael\c_opt rijndael\cpp_opt Реализация Эталонная реализация на С с набором тестов (авторы - Vincent Rijmen и Paulo Barreto) Оптимизированная реализация на С (авторы - Vincent Rijmen, Antoon Bosselaers и Paulo Barreto) Оптимизированная реализация на C++ (автор - Brian Gladman)
,1 *. ■:.^ГГ: № ■ЛГ: Часть Приложения char"' Гарри с удивлением наблюдал, как Думбльдор вставляет в замки третий, четвертый, пятый и шестой ключи, и всякий раз в сундуке обнаруживается разное содержимое, Джоан К. Роулинг, Гарри Поттер и Огненная чаша
ПРИЛОЖЕНИЕ А. Каталог функций на С А.1 Ввод/вывод, присваивание; преобразования, сравнения void cpyj (CLINT destj, CLINT srcj); void fswapj (CLINT a J, CLINT bj); int equj (CLINT aj, CLINT bj); int cmpj (CLINT a_l, CLINT bj); void u2clint_l (CLINT numj, USHORT ul); void u!2clintj (CLINT numj, ULONG ul); UCHAR* clint2bytej (CLINT nj, int *len); int byte2clint_i (CLINT nj, char *bytes, int len); char* xclint2strj (CLINT nj, USHORT base, int showbase); int str2clint_l (CLINT nj, char *N, USHORT b); dint * setmaxJ(CLINT n_I); unsigned int vcheckj (CLINT nj); char* verstrj (); ^ Присваивание значения srcj переменной destj Перестановка aj и b_l Проверка равенства значений a_l и bj Сравнение размеров переменных aj и b_l Преобразование типа USHORT в тип CLINT Преобразование типа ULONG в тип CLINT Преобразование типа CLINT в вектор байтов (согласно IEEE, Р1363, 5.5.1) Преобразование вектора байтов в тип CLINT (согласно IEEE, P1363, 5.5.1) Преобразование типа CLINT в строку символов в системе счисления с основанием base, с префиксом или без него Преобразование строки символов в системе счисления с основанием b в тип CLINT Присвоение целому числу n_l типа CLINT максимального значения Nmax Проверка правильности формата CLINT Версия библиотеки FLINT/C в виде строки символов; идентификатор 'а' указывает, поддерживается ли Ассемблер, идентификатор V -безопасный режим библиотеки FLINT/C
422 Криптография на Си и C++ в действии А.2 Основные арифметические операции jnt addj (CLINT a_l int uaddj (CLINT a_ int incJ(CLINTaJ) int subj (CLINT aj, int usubj (CLINT a_ int decj (CLINT aj) int mulj (CLINT aj int umulj (CLINT a_ int sqrj (CLINT aj, int divj (CLINT aj, int udivj (CLINT a_ CLINT bj, CLINT sj) J, USHORT b, CLINT sj) CLINT bj, CLINT sj) 1, USHORT b, CLINT sj) , CLINT bj, CLINT pj) J, USHORT b, CLINT pj) CLINT pj) CLINT bj, CLINT qj, CLINT rj) , USHORT b, CLINT qj, CLINT rj) Сложение: слагаемые а_1 и bj, сумма s_l Смешанное сложение: слагаемые aj и b, сумма sj Увеличение а_1 на единицу Вычитание: уменьшаемое а_1, вычитаемое bj, разность sj Смешанное вычитание: уменьшаемое а_1, вычитаемое Ь, разность s_l Уменьшение а_1 на единицу Умножение: сомножители а_1 и bj, произведение р_1 Смешанное умножение: сомножители а_1 и Ь, произведение р_1 Возведение а_1 в квадрат, результат р_1 Деление с остатком: делимое а_1, делитель bj, частное qj, остаток г_1 Смешанное деление с остатком: делимое а_1, делитель Ь, частное qj, остаток г_1 А.З Модульная арифметика int rnodj (CLINT d_l, CLINT nj, CLINT rj); USHORT umodj (CLINT dj, USHORT n); int mod2_l (CLINT d_l, ULONG k, CLINT rj); int mequj (CLINT aj, CLINT bj, CLINT mj); int maddj (CLINT aj, CLINT bj, CLINT cj, CLINT mj); Вычет d_l по модулю n_l, результат r__l Вычет dj по модулю n Вычет dj по модулю 2k Проверка сравнимости aj и b_l no модулю m_l Модульное сложение: сложение aj и b_l по модулю m_l/ результат c_l
Приложение А 423 int umaddj (CLINT a J, USHORT b, CLINT cj, CLINT mj); int msubj (CLINT aj, CLINT bj, CLINT cj, CLINT mj); int umsubj (CLINT a_l, USHORT b, CLINT cj, CLINT mj); int mmulj (CLINT a J, CLINT bj, CLINT с J, CLINT mj); void mulmonj (CLINT aj, CLINT bj, CLINT nj, USHORT nprime, USHORT logB_r, CLINT pj); void sqrmonj (CLINT aj, CLINT nj, USHORT nprime, USHORT logB_r, CLINT pj); int ummulj (CLINT a J, USHORT b, CLINT pj, CLINT mj); int msqrj (CLINT a J, CLINT с J, CLINT mj); int mexp5_l (CLINT basj, CLINT expj, CLINT pj, CLINT mj); int mexpkj (CLINT basj, CLINT expj, CLINT pj, CLINT mj); int mexp5m_l (CLINT basj, CLINT expj, CLINT pj, CLINT mj); int mexpkmj (CLINT basj, CLINT expj, CLINT pj, CLINT mj); int umexpj (CLINT basj, USHORT e, CLINT pj, CLINT mj); int umexpmj (CLINT basj, USHORT e, CLINT pj, CLINT mj); int wmexpj (USHORT bas, CLINT ej, CLINT pj, CLINT mj); Смешанное модульное сложение: сложение а_1 и b по модулю m_l, результат с_1 Модульное вычитание: разность а_1 и Ь_1 по модулю mj, результат с_1 Смешанное модульное вычитание: разность a J и b по модулю m_l, результат с_1 Модульное умножение: умножение а_1 на Ь_1 по модулю mj, результат с_1 Модульное умножение а_1 на bj по модулю п_1, произведение р_1 (метод Монтгомери, £logB_r-1 < р_, < £logB_r) Модульное возведение a_l в квадрат по модулю n_l, результат p_l (метод Монтгомери, B]ogB-r-] < n_l < e,ogB-r) Смешанное модульное умножение a_l на b по модулю m_l, произведение pj Модульное возведение aj в квадрат по модулю mj, результат c_l Модульное возведение в степень, арный метод 25- Модульное возведение в степень, 2*-арный метод, выделение динамической памяти через malloc() Возведение в степень по Монтгомери, 25- арный метод, нечетный модуль Возведение в степень по Монтгомери, 2/г-арный метод с параметром к, нечетный модуль, оптимальное значение для к определяется автоматически Модульное возведение в степень, показатель типа USHORT Возведение в степень по Монтгомери, нечетный модуль, показатель типа USHORT Модульное возведение в степень, основание типа USHORT
424 Криптография на Си и C++ в действии int wmexpmj (USHORT bas, CLINT ej, CLINT pj, CLINT mj); int mexp2_l (CLINT basj, USHORT e, CLINT pj, CLINT mj); int mexpj (CLINT basj, CLINT ej, CLINT pj, CLINT mj); Возведение в степень по Монтгомери, нечетный модуль, основание типа USHORT Модульное возведение в степень, показатель е - степень числа 2 Модульное возведение в степень; если модуль нечетный, автоматически вызывается функция mexpkmjQ, в противном случае - функция mexpkjQ А.4 Битовые операции int setbitj (CLINT a_l, unsigned int pos) int testbitj (CLINT a_l, unsigned int pos) int clearbitj (CLINT aj, unsigned int pos) void andj (CLINT aj, CLINT b_l, CLINT cj) void orj (CLINT aj, CLINT bj, CLINT cj) void xorj (CLINT aj, CLINT bj, CLINT с J) int shrj (CLINT aj) int shlj (CLINT a J) int shiftj (CLINT aj, long int noofbits) Проверка и установка бита в позиции pos в числе а_1 Проверка бита на позиции pos в числе a J Проверка и очистка бита на позиции pos в числе а_1 Побитовая операция AND a J и bj, результат с_1 Побитовая операция OR aj и bj, результат с_1 Побитовое сложение по модулю 2 (XOR) а_1 и Ь_1, результат с_1 Сдвиг а_1 на один бит влево 1 Сдвиг а_1 на один бит вправо 1 Сдвиг а_1 на noofbits битов Щ влево/вправо А.5 Теоретико-числовые функции unsigned issqrj (CLINT aj, CLINT bj) unsigned irootj (CLINT a_l, CLINT bj) Проверка того, является ли aj полным квадратом. Если да, то результат bj ■ квадратный корень Целая часть квадратного корня из aj| результат Ь_1
Приложение А 425 void gcd_l (CLINT aj, CLINT bj, CLINT gj) void xgcdj (CLINT aj, CLINT bj, CLINT gj, CLINT uj, int *sign_u, CLINT v_l, int *sign_v) void inv_l (CLINT aj, CLINT nj, CLINT gj, CLINT ij) void IcmJ (CLINT aj, CLINT bj, CLINT vj) int chinremj (unsigned noofeq, dint** koeffj, CLINT xj) int jacobij (CLINT a J, CLINT bj) int prootj (CLINT aj, CLINT pj, CLINT xj) int rootj (CLINT a J, CLINT pj, CLINT qj, CLINT xj) Int primrootj (CLINT xj, unsigned noofprimes, dint** primesj) USHORT sieve J (CLINT a J, unsigned noofsmallprimes) int primej (CLINT nj, unsigned noofsmallprimes, unsigned iterations) Наибольший обший делитель чисел aj и b_l, результат gj Наибольший обший делитель чисел aJ и bj и его представление в виде линейной комбинации с коэффициентами uj и vj, знаки коэффициентов sign_u и sign_v Наибольший обший делитель чисел а_1 и п J, а также обратное значение к a J mod n_l наименьшее обшее кратное чисел aj и Ь_1, результат vj Решение системы линейных сравнений, результат х_1 Символ Лежандра (Якоб и) a J no b_l Квадратный корень из aj mod pj, результат x_l Квадратный корень из aj mod pj*qj, результат х J Определение первообразного корня по модулю п, результат xj Метод пробного деления; деление числа aj на маленькие простые числа Сочетание проверки на простоту Миллера - Раб и на с методом пробного деления для числа nj А.6 Генерация псевдослучайных чисел dint* rand64j(void) void rand J (CLINT rJ, int 1) UCHAR ucrand64j (void) USHORT usrand64J (void) ULONG ulrand64j (void) Генератор случайных 64-разрядных чисел Линейный конгруэнтный генератор случайных чисел типа CLINT длины 1 Генератор случайных чисел типа UCHAR Генератор случайных чисел типа USHORT Генератор случайных чисел типа ULONG
426 Криптография на Си и C++ в действии dint* seed64_l (CLINT seed J) dint* ulseed64_l (ULONG seed) int randbitj (void) void randBBSJ (CLINT rj, int 1) UCHAR ucrandBBSJ (void) USHORT usrandBBSJ (void) ULONG ulrandBBSJ (void) void seedBBSJ (CLINT seed J) void ulseedBBSj (ULONG seed) A.7 Управление регистрами void set_noofregs_l (unsigned int nregs) int create_reg_l (void) dint* get_reg_l (unsigned int reg) int purge_reg_l (unsigned int reg) int purgeall_reg_l (void) void free_reg_! (void) dint* createj (void) void purgej (CLINT nj) void free J (CLINT nj); Инициализация функции rand64_l() значением типа CLINT Инициализация функции rand64_l() значением типа ULONG Генератор BBS Генерация с помошью BBS случайного числа типа CLINT длины I бит Генерация с помошью BBS случайного числа типа UCHAR Генерация с помошью BBS случайного числа типа USHORT Генерация с помошью BBS случайного числа типа ULONG Инициализация функции randbit_l() значением типа CLINT Инициализация функции randbitjf) значением типа ULONG Установление числа регистров Создание банка регистров типа CLINT Создание ссылки на регистр reg банка регистров Очистка регистра банка регистров путем затирания Очистка всех регистров банка регистров путем затирания Очистка всех регистров банка регистров путем затирания с последующим освобождением памяти Генерация регистра типа CLINT Очистка обьекта типа CLINT путем затирания Очистка регистра путем затирания <■ последующим освобождением памя'Н
ПРИЛОЖЕНИЕ В. Каталог функций C++ Р»»ши-*«л*^*™ "" .'J.»! ! Mi nii^^^^MEssa^Msssi inn i пиши оншна^нн B.1 Ввод/вывод, преобразования, сравнения: функции-члены класса LINT (void); LINT (const char* const str, const int base); LINT (const UCHAR* const byte, const int len); LINT (const char* const str); LINT (const LINT&); LINT (const signed int); LINT (const signed long); LINT (const unsigned char); LINT (const USHORT); UNT (const unsigned int); LINT (const unsigned long); LINT (const CLINT); Конструктор 1: создается неинициализированный объект класса LINT Конструктор 2: объект класса LINT создается из представления строкой разрядов в системе счисления с основанием base Конструктор 3: объект класса LINT создается из вектора байтов с разрядами в системе счисления с основанием 28согласно IEEE P1363, младшие разряды слева Конструктор 4: объект класса LINT создается из строки ASCII-символов, синтаксис как в языке С Конструктор 5: объект класса LINT создается из объекта класса LINT (конструктор копирования) Конструктор 6: объект класса LINT создается из целого числа типа int Конструктор 7: объект класса LINT создается из целого числа типа long Конструктор 8: объект класса LINT создается из целого числа типа unsigned char Конструктор 9: объект класса LINT создается из целого числа типа unsigned short Конструктор 10: объект класса LINT создается из целого числа типа unsigned int Конструктор 11: объект класса LINT создается из целого числа типа unsigned long Конструктор 12: объект класса LINT создается из целого числа типа CLINT
428 Криптография на Си и C++ в действии const LINT& operator = (const LINT& b); inline void disp (char* str); inline char* hexstr (void) const; inline char* decstr (void) const; inline char* octstr (void) const; inline char* binstr (void) const; char* Iint2str (const USHORT base, const int showbase = 0) const; UCHAR* Iint2byte (int* len) const; LINT& fswap (LINT& Devoid purge (void); static long flags (ostream& s); static long flags (void); static long setf (ostream& s, long int flags); static long setf (long int flags); static long unsetf (ostream& s, long int flags); static long unsetf (long int flags); static long restoref (ostream& s, long int flags); static long restoref (long int flags); Присваивание а <— b Отображение целого числа класса LINT (сначала выводится str) Представление целого числа класса LINT в шестнадцатиричном виде Представление целого числа класса LINT в десятичном виде Представление целого числа класса LINT в восьмеричном виде Представление целого числа класса LINT в двоичном виде Представление целого числа класса LINT в виде строки символов в системе счисления с основанием base с префиксом Ох (или Ob) при showbase > 0 Преобразование целого числа класса LINT в вектор байтов, результат len - длина вектора, согласно IEEE P1363, младшие разряды слева Замена неявного аргумента а аргументом b Очистка неявного аргумента а путем затирания Чтение статической переменной состояния LINT потока ostream s Чтение статической переменной состояния LINT потока ostream cout Установка битов переменной состояния LINT для потока ostream s, заданных в flags Установка битов переменной состояния LINT для потока к ostream cout, заданных в flags Очистка битов переменной состояния LINT потока ostream s, заданных в flags Очитка битов переменной состояния . LINT потока ostream cout, заданных в flags Сброс переменной состояния LINT потока ostream s к значению flags Сброс переменной состояния класса LINT, относящейся к ostream cout к значению flags
Приложение В В.2 Ввод/вывод, преобразования, сравнения: функции-друзья класса const int operator == (const LINT& a, const LINT& b); const int operator != (const LINT& a, const LINT& b); const int operator < (const LINT& a, const LINT& b); const int operator > (const LINT& a, const LINT& b); const int operator <= (const LINT& a, const LINT& b); const int operator >= (const LINT& a, const LINT& b); void fswap (LINT& a, LINT& b); void purge (LINT& a); char* Iint2str (const LIIMT& a, const USHORT base, const int showbase); UCHAR* Iint2byte (const LINT& a, int* len); ostream& LintHex (ostream& s); ostream& LintDec (ostream& s); ostream& LintOct (ostream& s); ostream& LintBin (ostream& s); ostream& LintUpr (ostream& s); Проверка равенства а == b Проверка того, что а != b Проверка неравенства а < b Проверка неравенства а > b Проверка неравенства а <= b Проверка неравенства а >= b Перестановка а и b Очистка путем затирания Представление а в виде строки символов в системе счисления с основанием base с префиксом Ох (или Ob) при showbase > 0 Преобразование а в вектор байтов, результат len -длина вектора, согласно IEEE P1363 младшие разряды слева Манипулятор для потока ostream для вывода числа класса LINT в шестнадцатиричном виде Манипулятор для потока ostream для вывода числа класса LINT в десятичном виде Манипулятор для потока ostream для вывода числа класса LINT в восьмеричном виде Манипулятор для потока ostream для вывода числа класса LINT в двоичном виде Манипулятор для потока ostream для вывода числа класса LINT в шестнадцатиричном виде с использованием прописных букв
430 ostream& LintLwr (ostream& s); ostream& LintShowbase (ostream& s); ostream& LintNobase (ostream& s); ostream& LintShowlength (ostream& s); ostream& LintNolength (ostream& s); LINT_omanip<int> SetLintFlags (int flag); LINT_omanip<int> ResetLintFlags (int flag); ostream& operator « (ostream& s, const LINT& In); ofstream& operator « (ofstream& s, const LINT& In); fstream& operator « (fstream& s, const LINT& In); ifstream& operator » (ifstream& s, LINT& In); fstream& operator » (fstream& s, LINT& In); В.З Основные операции: фун const LIIMT& operator ++ (void); m Криптография на Си и C++ в действии Манипулятор для потока ostream для вывода числа класса LINT в шестнадцатиричном виде с использованием строчных букв Манипулятор для отображения префикса Ох (соответственно Ob) в шестнадцатиричном (соответственно двоичном) представлении целого числа класса LINT Манипулятор для пропуска префикса Ох или Ob в шестнадцатиричном или двоичном представлении целого числа класса LINT Манипулятор для отображения двоичной длины выдаваемого целого числа класса LINT Манипулятор для пропуска двоичной длины выдаваемого целого числа класса LINT Манипулятор для установки битов переменной состояния LINT Манипулятор для очистки битов переменной состояния LINT Перегруженный оператор "поместить", выдающий целые числа класса LINT в выходной поток типа ostream Перегруженный оператор "поместить", записывающий целые числа класса LINT в выходной поток типа ostream Перегруженный оператор "поместить", записывающий целые числа класса LINT в поток типа fstream Перегруженный оператор "извлечь", читающий целые числа класса LINT из входного потока типа ifstream Перегруженный оператор "извлечь", читающий целые числа класса LINT из потока типа fstream i-члены класса Префиксное увеличение на единицу ++а;
Приложение В 431 const LINT operator ++ (int); const LINT& operator -- (void); const LINT operator -- (int); const LINT& operator += (const LINT& b); const LINT& operator -= (const LINT& b); const LINT& operator *= (const LINT& b); const LINT& operator /= (const LINT& b); const LINT& operator %= (const LINT& b); const LINT& add (const LINT& b); const LINT& sub (const LINT& b); const LINT& mul (const LINT& b); const LINT& sqr (void); const LINT& divr (const LINT& d, LINT& r); B.4 Основные операции: Постфиксное увеличение на единицу а++; Префиксное уменьшение на единицу —а; Постфиксное уменьшение на единицу а-; Сложение и присваивание: а += Ь; Вычитание и присваивание: а -= Ь; Умножение и присваивание а *= Ь; Деление и присваивание: а /= Ь; Вычисление остатка отделения и присваивание: а %= Ь; Сложение: с = a.add (b); Вычитание: с = a.sub (b); Умножение: с = a.mul (b); Возведение в квадрат: с = a.sqr (b); Деление с остатком: quotient = dividend.divr (divisor, rest) кции-друзья класса const LINT operator + (const LINT& a, const LINT& b); const LINT operator - (const LINT& a, const LINT& b); const LINT i operator * (const LINT& a, const LINT& b); const LINT operator / (const LINT& a, const LINT& b); const LINT operator % (const LINT& a, const LINT& b); Сложение: с Вычитание: Умножение: Деление: с = Вычисление с = а % Ь; = а + с = а- с = а а/Ь; Ь; -Ь; *Ь; остатка отделения:
432 Криптография на Си и C++ в действии const LINT add (const LINT& a, const LINT& b); const LINT sub (const LINT& a, const LINT& b); const LINT mul (const LINT& a, const L1NT& b); const LINT sqr(constLINT&a); const LINT divr (const LINT& a, const LINT& b, LINT& r); Сложение: с = add (a, b); Вычитание: с = sub (a, b); Умножение: с = mul (a, b); Возведение в квадрат: b = sqr (a); Деление с остатком: quotient = divr (dividend, divisor, rest); B.5 Модульная арифметика: функции-члены класса const LINT& mod (const LINT& m); const LINT& mod2 (const USHORT u); const int mequ (const LINT& b, const LINT& m) const; const LINT& madd (const LINT& b, const LINT& m); const LINT& msub (const LINT& b, const LINT& m); const LINT& mmul (const LINT& b, const LINT& m); const LINT& msqr (const LINT& m); const LINT& mexp (const LINT& e, const LINT& m); const LINT& mexp (const USHORT u, const LINT& m); const LINT& mexp5m (const LINT& e, const LINT& m); const LINT& mexpkm (const LINT& e, const LINT& in); const LINT& mexp2 (const USHORT u, const LINT& m); Вычисление остатка отделения b = a.mod (m); Вычисление остатка отделения на 2U b = a.mod (u); Сравнение вычетов а и b по модулю m if (a.mequ (b, m)) ... Модульное сложение с = a.madd (b, m); Модульное вычитание с = a.msubfb, m); Модульное умножение с = a.mmul (b, m); Модульное возведение в квадрат с = a.msqr (m); Модульное возведение в степень |Ё| по Монтгомери для нечетного К модуля т, с = а.техр (е, т); Я| Модульное возведение в степень по Монтгомери для нечетного модуля m и показателя типа USHORT, с = а.техр (и, т); Модульное возведение в степень по Монтгомери для нечетного модуля т, с = а.техр5т (е, т); Модульное возведение в степень по Монтгомери для нечетного модуля т, с = a.mexpkm (e, т); Модульное возведение в степень для показателя вида 2й, с = а.техр2 (и, т);
Приложение В 433 ^я В.6 Модульная арифметика: функции-друзья класса const LINT mod (const LINT& a, const LINT& m); const LINT mod2 (const LINT& a, const USHORT u); const int mequ (const LINT& a, const LINT& b, const LINT& m); const LINT madd (const LINT& a, const LINT& b, const LINT& m); const LINT msub (const LINT& a, const LINT& b, const LINT& m); const LINT mmul (const LINT& a, const LINT& b, const LINT& m); const LINT msqr (const LINT& a, const LINT& m); const LINT mexp (const LINT& a, const LINT& e, const LINT& m); const LINT mexp (const USHORT u, const LINT& e, const LINT& m); const LINT mexp (const LINT& a, const USHORT u, const LINT& m); const LINT mexp5m (const LINT& a, const LINT& e, const LINT& m); const LINT mexpkm (const LINT& a, const LINT& b, const LINT& m); const LINT mexp2 (const LINT& a, const USHORT u, const LINT& m); Вычисление остатка отделения b = mod (a, m); Вычисление остатка отделения на 2U b = mod (a, u); Сравнение вычетов а и b по модулю m if (mequ (a, b, m)) ... Модульное сложение с = madd (a, b, m); Модульное вычитание с = msub(a, b, m); Модульное умножение с = mmul (a, b, m); Модульное возведение в квадрат с = msqr (a, m); Модульное возведение в степень по Монтгомери для нечетного модуля т, с = техр (а, е, т); Модульное возведение в степень по Монтгомери для нечетного модуля m и основания типа USHORT, с = mexp (u, e, m); Модульное возведение в степень по Монтгомери для нечетного модуля m и показателя типа USHORT, с = mexp (a, u, m); Модульное возведение в степень по Монтгомери, только для нечетного модуля т, с = техр5т (а, е, т); Модульное возведение в степень по Монтгомери, только для нечетного модуля т, с = mexpkm (а, е, т); Модульное возведение в степень для показателя вида 2й, с = техр2 (а, и, т); В.7 Битовые операции: функции-члены класса const LINT& operator л= (const LINT& b); XOR и присваивание а л= Ь; 15-1697
434 Криптография на Си и C++ в действии const LINT& operator I = (const LINT& b); const L1NT& operator &= (const L1NT& b); const LINT& operator «= (const int \); const LINT& operator »= (const int i); const LINT& shift (const int i); const LINT& setbit (const unsigned int i); const LINT& clearbit (const unsigned int i); const int testbit (const unsigned int i) const; Vi" * (ft* &TV ' fro Hv ■WOT' OR и присваивание a 1= b; AND и присваивание a &= b; Сдвиг влево и присваивание а «= i; Сдвиг вправо и присваивание а «- i; Сдвиг (влево или вправо) на i битов a.shift (i); Установка бита на i-ю позицию a.setbit (i); Очистка бита на i-й позиции axlearbit (i); Проверка значения бита на i-й позиции a.testbit (i); В.8 Битовые операции: функции-друзья класса const LINT operator л (const LINT& a, const L1NT& b); const LINT operator I (const LINT& a, const LINT& b); const LINT operator & (const LINT& a, const L1NT& b); const LINT operator « (const LINT& a, const int i); const LINT operator » (const L1NT& a, const int i); const LINT shift (const LINT& a, const int i); :.'-%/ '■ XOR с = а л b; OR с = a I b; AND с = a & b; Сдвиг влево b = a « i; Сдвиг вправо b = a » i; Сдвиг (влево или вправо) на i битов b = shift (a, i); В.9 Теоретико-числовые функции-члены класса const unsigned int Id (void) const; const int iseven (void) const; const int isodd (void) const: Вычисление Llog 2 a J Проверка того, делится ли а на 2: true, если а четное Проверка того, делится ли а на 2: true, если а нечетное
Приложение В : g|t 435 const LINT .p.: фА issqr (void) const; 1л' ^; const int isprime (void) const; const LINT gcd (const LINT&b); const LINT xgcd (const LINT& b, LINT& u, int& sign_u, LINT& v, int& sign_v) const; const LINT inv (const LINT& b) const; const LINT Icm (const LINT& b) const; an.: const int jacobi (const LINT& b) const; const LINT root (void) const; const LINT root (const LINT& p) const; ;£p &ty const LINT root (const L1NT& p, const LINT& q) constjHS T it const int twofact (LINT& odd) const; const LINT chinrem (const LINT& m, const LINT& b, const LINT& n) const; Проверка того, является ли а полным квадратом Проверка числа а на простоту Вычисление наибольшего обшего делителя чисел а и b Расширенный алгоритм Евклида, вычисление наибольшего обшего делителя чисел а и b, u и v - абсолютные значения коэффициентов линейной комбинации g = sign_u*u*a + sign_v*v*b Вычисление мультипликативно обратного к а по модулю b Вычисление наименьшего обшего кратного чисел а и b -d) Вычисление символа Якоб Вычисление целой части квадратного корня из а Вычисление квадратного корня из а по модулю нечетного простого числа р Вычисление квадратного корня из а по модулю p*q, числа р и q - простые нечетные Определение четной части числа а, в odd - нечетная часть а Вычисление решения х системы линейных сравнений х = a mod m и х = b mod n (если такое решение существует) В.10 Теоретико-числовые функции-друзья класса const unsigned ld(constLINT&a); const int iseven (const LINT& a); const int isodd(constLINT&a); const LINT issqr (const LINT& a); ,. .Ч(СГ *и~№\- ■ •••Ут-;ч^ ■ ■**#?'■" -" Вычисление Llog2(a)j Проверка того, делится ли а на 2: true, если а четное Проверка того, делится ли а на 2: true, если а нечетное Проверка того, является ли а полным квадратом
436 Криптография на Си и C++ в действии const int isprime (const LINT& a); const LINT gcd (const LINT& a, const LINT& b); const LINT xgcd (const LINT& a, const LINT& b, LINT& u, int& sign_u, LINT& v, int& sign_v); 1 const LINT inv (const LINT& a, const LINT& b); const LINT Icm (const LINT& a, const LINT& b); const int jacobi (const LINT& a, const LINT& b); const LINT root (const LINT& a); const LINT root (const L1NT& a, const LINT& p); const LINT root (const LINT& a, const LINT& p, const LINT& q); const LINT chinrem (const unsigned noofeq, LINT** coeff); const LINT primroot (const unsigned noofprimes, LINT** ,чт primes); * l Г J v.r. const int twofact (const LINT& even, LINT& odd); const LINT findprime (const USHORT I); Проверка числа а на простоту Вычисление наибольшего обшего делителя чисел а и b Расширенный алгоритм Евклида, вычисление наибольшего обшего делителя чисел а и b, u и v - абсолютные значения коэффициентов линейной комбинации g = sign_u*u*a + sign_v*v*b Вычисление мультипликативно обратного к а по модулю b Вычисление наименьшего обшего кратного чисел а и b Вычисление символа Якоби \т \ Вычисление целой части квадратного корня из а Вычисление квадратного корня из а по модулю нечетного простого числа р Вычисление квадратного корня из а по модулю p*q, числа р и q - простые нечетные Вычисление решения системы линейных сравнений. В coeff содержится вектор указателей на объекты класса LINT как коэффициенты аь 1ти, а2, гп2, а3, т3, ... системы сравнений, состоящей из noofeq сравнений вила х э a, mod m,. Вычисление первообразного корня по модулю р. В noofprimes содержится число различных простых делителей порядка группы - числа р - 1, в primes - вектор указателей на объекты класса LINT: сначала р - 1, затем простые 1 е1 делители pt, ..., р^ числа р- 1 = Р] '-...• р^А, где к = noofprimes. Вычисление четной части числа а, в odd содержится нечетная часть числа а. Генерация простого числа р длины I бит, то есть 21 - 1 < р < 21.
Приложение В 437 const LINT findprime (const USHORT I, const LINT& f); const LINT findprime (const LINT& pmin, const LINT& pmax, const LINT&f); const LINT nextprime (const LINT& a, const LINT& 0; const LINT extendprime (const USHORT I, const LINT& a, const LINT& q, const LINT& f); const LINT extendprime (const LINT& pmin, const LINT& pmax, const LINT& a, const LINT& q, const LINT& f); const LINT strongprime (const USHORT I); const LINT strongprime (const USHORT I, const LINT& f); const LINT strongprime (const USHORT I, const USHORT It, const USHORT Ir, const USHORT Is, > const LINT&f); const LINT strongprime (const LINT& pmin, const LINT& pmax^ const LINT& f); const LfNT strongprime (const LINT& pmin, const LINT& pmax, const USHORT It, const USHORT Ir, const USHORT Is, const LINT& f); Генерация простого числа р длины I бит, то есть 21 - 1 < р < 21, такого, что НОА(р -1,0 = 1, где число f- нечетное Генерация простого числа р такого, что pmin < р < ртах и НОД(р -1,0 = 1, где число f- нечетное Генерация наименьшего простого числа р, превышающего число а и такого, что НОД(р - 1, 0 = 1, где число f- нечетное Генерация простого числа р длины I бит, то есть 21 - 1 < р < 21, такого, что р = a mod q и НОА(р - 1, 0 = 1, где число f - нечетное Генерация простого числа р такого, что pmin < р < pmax, p s a mod q и НОА(р —1,0 = 1, гле число f - нечетное Генерация сильного простого числа р длины I бит, то есть 21 - 1 < р < 21. Генерация сильного простого числа р длины I бит, то есть 21 - 1 < р < 21, такого, что НОД(р -1,0 = 1, где число f- нечетное Генерация сильного простого числа р длины I бит, то есть 21 - 1 < р < 2х, такого, что НОД(р - 1, 0 = 1, где число f- нечетное; при этом длины Ir, It, Is простых делителей г числа р - 1, t числа г - 1, s числа р + 1, соответственно, удовлетворяют условиям: It g j, Is ~ Ir g S двоичной длины числа р Генерация сильного простого числа р такого, что pmin < р < ртах иНОД(р-1,А = 1, где число f - нечетное; при этом длины Ir, It, Is простых делителей г числа р - 1, t числа г - 1, s числа р + 1, соответственно, удовлетворяют условиям: It g j, Is = Ir g S двоичной длины числа pmin Генерация сильного простого числа р такого, что pmin < р < ртах и НОД(р - 1, 0 = 1, где число f - нечетное; при этом Ir, It, Is -длины простых делителей г числа р - 1, t числа г - 1, s числа р + 1, соответственно.
438 Криптография на Си и C++ в действии В.11 Генерация псевдослучайных чисел void seedl (const LINT& seed); LINT randl (const int I); . j. . LINT randi (const LINT& rmin, const LINT& rmax); int seedBBS (const LINT& seed), LINT randBBS (const int I); LINT randBBS (const LINT& rmin, I лт>: m, '■■■ i .ХвГПП const LINT& rmax); Инициализация 64-разрялного линейного конгруэнтного генератора случайных чисел с помошью начального значения seed Генерация случайного числа класса LINT длины I бит Генерация случайного числа г j класса LINT, где rmin < г < rmax Инициализация генератора BBS случайных чисел с помошью начального значения seed Генерация случайного числа класса LINT длины I бит ' Генерация случайного числа г класса LINT, где rmin < г < rmax В.12 Прочие функции LINT_ERRORS Get_Warning_Status (void); static void [жр}:- SetJ_INT_Error Handler (void (*)(LINT_ERRORS err, const char* const, const int, const int)); '. ,/'b;U;.;- , ' Запрос состояния ошибки объекта класса LINT Вызов пользовательской программы обработки ошибок для LINT-операиий. Зарегистрированная программа используется вместо стандартного LINT-обработчика ошибок panic(). Отмена регистрации пользовательской программы и одновременное возобновление использования программы panic() осуществляется вызовом функции Set_LINTJrror_Handler (NULL).
ПРИЛОЖЕНИЕ С. •* Макросы С.1 Коды ошибок и значения состояний E_CLINT_DBZ E_CLINT_OFL E_CLINTJJFL E_CLINT_MAL E_CLINT_NOR E__CLlNT_BOR E_CLINT_MOD E_CLINT_NPT E__VCHECK_OFL E_VCHECK_LDZ E_VCHECK_MEM -1 -2 -3 -4 -5 -6 -7 -8 1 2 -1 Деление на нуль Переполнение Потеря значащих разрядов Ошибка распределения памяти Регистр недоступен Неверное основание в str2clint_l() Четный модуль в процедуре приведения по Монтгомери В качестве аргумента передан нулевой указатель Предупреждение функции vcheck_l(): число слишком большое Предупреждение функции vcheck_l(): нули в старших разрядах Ошибка функции vcheck_l(): нулевой указатель С.2 Дополнительные константы BASE BASEMINONE DBASEMINONE BASEDIV2 NOOFREGS BITPERDGT 0x10000 OxffffU OxffffffffUL 0x8000U 16U 16UL Основание В - 216 системы счисления в типе CLINT В-1 в2-^ 1В/2] Стандартное число регистров в банке регистров Число двоичных разрядов в CLINT-разряде
440 Криптография на Си и C++ в действии LDBITPERDGT CLINTMAXDICIT CLINTMAXSHORT CL1NTMAXBYTE CLINTMAXBIT г0_1,..., г15_! FLINT_VERMAJ FLINT_VERMiN FLINT_VERSION FLINT SECURE 4U - 256U ~- (CL1NTMAXDIG1T+1) (CLINTMAXSHORT « 1) (CLINTMAXDICIT «4) get_regj(0),..., get_reg_l(15) ((FLINT_VERMAJ«8) + FLINT_VERMIN) 0x73, 0 Логарифм по основанию 2 otBITPERDGT Максимальное число разрядов в CLINT-обьекте в системе счисления с основанием В Число разрядов типа USHORT, выделенных под CLINT-объект Число байтов, выделенных под CLINT-объект Максимальное число двоичных разрядов в CLINT-обьекте Указатель на CLINT-регистры 0, ..., 15 Старшая цифра версии библиотеки FLINT/C Младшая цифра версии библиотеки FLINT/C Номер версии библиотеки FLINT/C Идентификатор У или '' безопасного режима библиотеки FLINT/C С.З Макросы с параметрами clint2str_l (n I, base) ^CLINT2STR_L (n__l, base) DiSPJ_ (S, A) HEXSTR_L (nj) т а ,V. :.■ , : DECSTR_L (n) ■. ж ■ * OCTSTR_L (nj) xclint2strj((nj),(base),0) printf("%s%s\n%u bit\n\n", (S), HEXSTR_L(A), IdJ(A)) xclint2str_l((nj), 16,0) ■U-' iU; xclint2strj((n), 10, 0) xclint2strj((nj), 8, 0) Представление CLINT-обьекта в виде строки символов без префикса Стандартный вывод CLINT-обьекта Преобразование CLINT-обьекта в шестнадцатиричное представление Преобразование CLINT-обьекта в десятичное представление Преобразование CLINT-обьекта в восьмеричное представление
Приложение С 441 BINSTR_L(nJ) SET_L (nj, ul) SETZEROJ. (nj) SETONE_L (nj) SETTWO_L (nj) ASSIGN_L (aj, bj) ANDMAX_L(a_l) DIGITSJ. (n_l) SETDIGITS_L (nj, I) INCDIGITSJ. (nj) DECDiGITS_L (n_l) LSDPTRJ. (nj) MSDPTR^L (nj) RMLDZRS.L (nj) SWAP (a, b) SWAP_L (aJ, bj) LTJ. (aj, bj) LE __L (aj, bj) GT_L (aj, b_l) CE_L (a_\, bj) GTZ_L"'(aJ) EQZJ- (aj) EQONE_L (a_l) xclint2strj«n_l)/ 2, 0) ul2clintj«nj), (ul)) (*(nj) = 0) u2clint_l((nj), 1U) u2clintj((nj), 2U) cpyj((a_l), (bj)) SETDlGITSJ_((aJ), (MIN(DIGITS_L(aJ), \ (USHORT)CUNTMAXDIGIT)); RMLDZRS_L((aJ)) (*(n_D) (*(nJ) = (USHORT)(l)) (++*(nj)) (~*(nj)) ((n_l) + 1) ((nj) + DIGITS_L(n_l)) while((DIGITS_L(nJ) > 0) && (*MSDPTR_L(n_l) == 0)) {DECDlGITS_L(n_i);) ((a)Mb),(b)A=(a),(a)*=(b)) (xorJ((aJ),(b_l),(aJ)), xorJ((b_l),(aJ),(bJ)), xorJ((aJ),(b_l),(aJ))) (cmpj((a_l), (b_l))==-1) (cmpj((a_l)/(bj))<1) (cmpj((aj),(b_l))==1) (cmpj((aj),(bj))>-1) (cmp_l((aj), nulj)==l) (equ_l((aj), nulj)==1) <equJ«aJ),oneJ)==1> Преобразование CLINT-обьекта в двоичное представление Присваивание n_lf-ULONGul Установить nj в 0 Установить nj в 1 Установить nj в 2 Присваивание а_1 <— bj Приведение по модулю (Nmax + 1) Прочитать число разрядов nj в системе счисления с основанием В Установить число разрядов nj в1 Увеличить число разрядов на 1 Уменьшить число разрядов на 1 Указатель на младший разряд CLINT-обьекта Указатель на старший разряд CLINT-обьекта Удаление ведущих нулей CLINT-обьекта Перестановка Перестановка двух значений типа CLINT Проверка условия a_l < b_l Проверка условия aj < bj Проверка условия aj > bj Проверка условия a_l > bj Проверка условия a_l > 0 Проверка условия aj == 0 Проверка условия a_l == 1
• 442 Криптография на Си и C++ в действии MINJ. (aj, b_l) MAX_L (aj, b_l) ISEVEN_L (nj) J ISODD L (n 1) «Л'Ж I .:ITM2 MEXP_L (aj, ej, pj, nj) MEXP^L (aj, ej, pj, nj) IN1TRAND64_LT() INITRANDBBSJ_T() ISPRIME_L (n_l) ZEROCLINTJ. (nj) (LT_L((a_l), (b_D) ? (aj) :(bj)) (GT_L((a_l), (b_i)) ? (aj) :(b_D) (DIGITS_L(n_l)==0 il (DIGITSJ_(n_l)>0&& (*(LSDPTR_L(n I)) & 1 U) == 0)) (DIGITS J_(n_l) > 0 && (*(LSDPTRJ_(nJ)) & 1 U) == 1) mexpkj((aj), (ej), (pj), (nj)) mexp5_l«aj), (ej), (pj), (nj)) mexpkmj((aj), (ej), (pj), (nj)) mexp5mj((aj), (ej), (pj), (nj)) seed64j((unsigned long) time(NULL)) seedBBSJ((unsigned long) time(NULD) primej((nj), 302, 5) memset((A), 0, sizeof(A)) Минимум из двух значений типа CLINT Максимум из двух значений типа CLINT Проверка того, является ли nj четным Проверка того, является ли nj нечетным Возведение в степень Альтернативные варианты возведения в степень Инициализация генератора rand64_l() случайных чисел с помошью системных часов Инициализация генератора randbit_l() случайных битов с помошью системных часов Проверка на простоту с фиксированными параметрами Затирание CLINT-переменной ""■Ц1>*> ■ -U**l> г^1:деи,^ KJ^Kj- яй'яиюу ^Н:-;' жу.( сне,-* ■оу бжз' ■■ \^>' ■ 0> ж ЦП ^ .:...i{.>iJi.A ":{':v:Jb**Bs*»^" i.
ПРИЛОЖЕНИЕ D. Время вычислений Q4X Таблица D.1. Время вычислений лля некоторых С-функиий (без ассемблера) В таблицах D.1 и D.2 приведено время вычислений для некоторых функций пакета FLINT/C. Вычисления проводились на процессоре Pentium III, 500 МГц, 64 Мбайт ОЗУ. Время измерялось для п операций, и результат делился на п. Число и, в зависимости от типа функции, варьировалось от 100 до 5 000 000. В таблице D.3 приведено для сравнения время вычислений для некоторых функций библиотеки многоразрядной арифметики GNU GMP (GNU Multi Precision Arithmetic Library, версия 2.0.2), см. стр. 448. addj mulj sqr_f divj * mmulj msqrj mexpkj mexpkmj 128 3,8-10'7 1,9-10 6 1,5-10"6 2,6-1 0"6 7,5-10'6 7,3-10"6 1,2-10"3 5,4-10-* 256 5,5-10"7 5,1-10"6 3,7-10"6 5,8-10"6 2,1-10"5 1,9-10~5 6,4-10~3 2,9-10"3 512 8,8-10"7 1,6-10"5 1,Ы(Г5 7,6-10"5 6,6-1 О"5 6,1-10'5 3,8-10"2 1,7-10"2 768 1,2-10"6 3,3-10"5 2,Ы0~5 2,7-1 О"5 1,4-1 (Г4 1,2-10"4 1,2-1 0м 5,2-10"2 1024 1,5-1 (Г6 5,6-1 О5 3,5-10~5 5,1-10~5 2,3-1(Г4 2,1-1 (Г4 2,МО'1 1,2-10'1 2048 2,2-10~6 2,1-10'4 1,3-10"4 7,8-10"4 8,9-1 О"4 8,1-10"4 1,9 8,6-10'1 Лля функции div_l число разрядов относится к делимому, длина делителя — в два раза меньше. 9\ "Щ; Сразу видно, что возведение в квадрат быстрее, чем умножение. Видно даже преимущество функции mexpkmJO, использующей возведение в квадрат по Монтгомери, - она работает почти в два раза быстрее, чем функция mexpk_l(). Таким образом, один шаг алгоритма RSA с ключом длины 2048 бит при использовании китайской теоремы об остатках (см. стр. 225) занимает четверть секунды. В таблице D.2 приведены результаты, полученные при использовании ассемблерных подпрограмм. В этом случае скорость модульных функций увеличивается примерно на 70%. Разрыв между умножением и возведением в квадрат остается на уровне 50%.
444 Криптография на Си и C++ в действии Таблииа D.2. Время вычислений лля некоторых С-функиий (ассемблер 80x86) mul_l sqrj divj * MmulJ msqrj mexpkj mexpkm_i 128 4,3-10~6 2f5-10-e 2,7Л О'6 8,0-10"6 7,2-10"6 1,3-10"3 7,6-10"4 256 6,9*10"6 4,6-10"6 3,5-10"6 1,3-1(Г5 1,1-10"5 4,3-10"3 3,3-10'3 512 1,5-10"5 1,0-1 (Г5 7,7-10"6 3,3*10"5 2,9-10"5 1,9*10-2 1,7-10"2 768 2,9-1 О"5 1,8-10"5 1,0-10~5 6,4-10"5 5,6-10"5 5,3-10"2 4,9-10"2 1024 4,7-10'5 2,9-10~5 1,9*10"5 1,0-1 О"4 9,0-10"5 1,1-10"1 1,1-10"1 2048 1,6-10"4 9,5-Ю"5 6,0-10~5 3,8-10м 3,1-10"^ 8,7-10"1 7,8*10"1 Для функции div I число разрядов относится к делимому, длина делителя — в два раза меньше. -<№ Таблииа D.3. Время вычислений лля некоторых GMP-функиий (ассемблер 80x86) В ассемблерном варианте нет функций mulmonJO и sqrmonJO, a функция mexpk_l() по скорости вплотную приближается к функции mexpmJO возведения в степень по Монтгомери. Здесь возможны дальнейшие улучшения (см. главу 18) за счет использования соответствующих ассемблерных расширений. Сравнивая функции пакетов FLINT/C и GMP (см. таблицу D.3), мы видим, что умножение и деление в GMP выполняются соответственно на 30% и 40% быстрее, чем аналогичные функции из FLINT/C. Что касается модульного возведения в степень, то здесь обе библиотеки показывают примерно одинаковую скорость; то же самое справедливо и для аргументов длины 4096 бит. Поскольку GMP считается наиболее быстрой на сегодняшний день библиотекой целочисленной арифметики, нам грех жаловаться на такой результат. шшттшшшшшшшшш mpz_add mpz_mul mpz_mod * mpz_powm 128 2,4-10"7 9,8-10"7 5,2-10"7 4,5*10"" 256 3,2-10"7 3,0-10"6 1,8-10"6 2,6-10"3 512 3,6-10'7 1,1-Ю-5 5,0*10 6 1,710 2 768 4,2-10"7 2,2-10~5 6,4-10"6 5,2-10"2 1024 4,5-10~7 4,1-10"5 1,6-10"5 1,7-10'1 2048 6,9-10"7 4,8-Ю'5 4,0*10'5 7,8-10"1 Для функции mpz_mod число разрядов относится к делимому, длина делителя — в два раза меньше.
ПРИЛОЖЕНИЕ-Е. ^ Условные обозначения IN 1N+ а a=b a%b a <r~ b \a\ a \b s\b a e= b mod n аЩ b mod я HOA(a, b) HOK(a, b) ф(п) O(-) UJ M p множество неотрицательных целых чисел 0, 1, 2, 3, ... множество положительных целых чисел 1, 2, 3, ... множество целых чисел ..., -2, -1,0, 1,2, 3, ... кольцо классов вычетов по модулю п надмножеством целых чисел (глава 5) конечное поле из рп элементов класс вычетов а + пЖ в кольце Z„ а приблизительно равно 6 а приблизительно равно b и при этом меньше b переменной а присвоить значение b абсолютное значение а а делит b без остатка а не делит b а сравнимо с b по модулю п, то есть п \ (а - Ь) а несравнимо с Ь по модулю п, то есть п\{а -Ь) наибольший обший делитель чисел а и b (п. 10,1) наименьшее обшее кратное чисел а и Ь (п. 10.1) функция Эйлера (п. 10.2) «О-большое». Аля двух вещественных функций f и g, гле g(x) > 0, пишут f- 0(g) и говорят «/"порядка О-большое OTg», если сушествует константа С такая, что fix) < Cg(x) для всех достаточно больших х. символ Якоби (п. 10.4.1) наибольшее целое число, меньшее либо равное х наименьшее целое число, большее либо равное х множество вычислительных задач, которые могут быть решены за полиномиальное время
446 Криптография на Си и C++ в действии NP в "~ МАХе -'"'[' МАХ2 >ш*лн,* множество вычислительных задач, которые могут быть решены недетерминированным алгоритмом за полиномиальное время логарифм по основанию Ь от х В = 216, основание системы счисления для представления объектов типа CLINT максимальное число разрядов, допустимое для CLINT-обьекта в системе счисления с основанием В максимальное число разрядов, допустимое для CLINT-объекта в системе счисления с основанием 2 наибольшее натуральное число, представимое объектом типа CLINT Ы: *■■?•>*:• £ шн*мщ*аы& viibtA' 'Wjtt';
t ■ ■ ■ ПРИЛОЖЕНИЕ?. Арифметические и теоретико-числовые пакеты Если читатель все еще сомневается в привлекательности и полезности алгоритмической теории чисел, ему стоит лишь посмотреть, сколько веб-сайтов посвящено этой теме, - и сомнительными окажутся уже сами сомнения. Просто наберите в Вашей любимой по- •v- исковой системе строку «теория чисел» (или «number theory») - появятся тысячи ссылок, лишь малая толика из которых упомянута в этой книге. Многие из этих веб-сайтов содержат ссылки на пакеты программ, кое-где их даже можно свободно скачать. Это разнообразные функции, связанные с арифметикой больших целых чисел, алгеброй, теорией групп и теорией чисел, созданные усилиями многочисленных профессионалов и любителей. Огромный список ссылок на такие пакеты можно найти на сайте Number Theory Web Page, поддерживаемом Кейт Мэтьюс (Keith Matthews) (University of Queensland, Брисбен, Австралия). Этот сайт читатель найдет на http://www.math.uq.edu.au/-krm/web.html, ^ американский сайт-зеркало *Е http://www.math.uga.edu/~ntheory/web.html ф; и британский сайт-зеркало http://www.dpmms.cam.ac.uk/Number-Theory-Web/web.html. Там же можно найти ссылки на университеты и исследовательские институты, а также на публикации по различным вопросам теории t° чисел. Этот сайт - просто драгоценный клад. Перечислим теперь некоторые доступные программные пакеты: &. • ARIBAS - интерпретатор, исполняющий арифметические и теоре- .л- тико-числовые функции с большими числами. В ARIBAS на языке ■;* Паскаль реализованы алгоритмы из [Fors]. ARIBAS может быть ис- ,,, пользован как дополнение к этой книге, его можно получить через анонимный ftp в каталоге pub/forster/aribas, ftp.mathematik.uni- muenchen.de или на http://www.mathematik.uni-muenchen,de/~forster. • CALC от Кейт Мэтьюс - программа, позволяющая проводить вычисления со сколь угодно длинными целыми числами. Команды г вводятся в командной строке, CALC выполняет их и отображает результат. CALC «знает» более 60 теоретико-числовых функций. ^ Пакет реализован на языке С версии ANSI С и использует для ана-
448 Криптография на Си и C++ в действии **л **/■> Л/ ? ■■'. А' №•■ З'М # **«£* ■ if) ЖЯ sqj£; лиза командной строки анализатор YACC или BISON. Calc можно взять с ftp://www.maths.uq.edu.au/pub/krm/calc/. GNU MP, или GMP, из проекта GNU - это переносимая библиотека С, в которой реализована арифметика сколь угодно больших целых, а также рациональных и вещественных чисел. Благодаря использованию ассемблера, библиотека GMP демонстрирует прекрасную производительность для целого ряда процессоров. GMP доступна через ftp на www.gnu.org, prep.ai.mit.edu, www.leo.org, а также на сайтах-зеркалах проекта GNU. LiDIA - библиотека программ, разработанная в Техническом университете Дармштадта (Technical University Darmstadt) для теоретико-числовых вычислений. LiDIA - это целая коллекция оптимизированных функций, позволяющая выполнять вычисления в Z, Q, R, С, F2n, ¥рП, а также смешанные вычисления. Здесь реализованы также современные алгоритмы разложения на множители, алгоритм минимизации базиса решетки, алгоритмы линейной алгебры, методы вычислений в числовых полях, а также полиномиальная арифметика. LiDIA может взаимодействовать с другими вычислительными пакетами, в том числе с пакетом GMP. Собственный интерпретируемый язык LC пакета LiDIA поддерживает C++ и тем самым облегчает переход к транслируемым программам. Поддерживаются все платформы, допускающие использование длинных имен файлов и имеющие подходящий компилятор C++, в том числе Linux 2.0.x, Windows NT 4.0, OS/2 Warp 4.0, HPUX-10.20, Sun Solaris 2.5.1/2.6. v Есть и перенесенная версия для Apple Macintosh. Библиотеку LiDIA можно найти на http://www.informatik.tu-darmstadt.de^l/UDIA. V Numbers от Ivo Diintsch - это библиотека объектных файлов, со- Лм$Ь\'г^!ц\&1 держащая основные теоретико-числовые функции для чисел длиной до 150 десятичных знаков. Функции, написанные на Паскале, и интерпретатор, также входящий в состав библиотеки, предназначены для студентов и позволяют получать нетривиальные вычислительные примеры. Страница Numbers находится по адресу http://archives.math.utk.edu/software/msdos/ number.theory/num202d/.html. • PARI - теоретико-числовой пакет, разработанный Генри Коэном (Henri Cohen) и др. В этом пакете реализованы алгоритмы, представленные в [Cohe]. PARI можно использовать и в качестве интерпретатора, и в качестве библиотеки функций для включения в программы. Благодаря использованию ассемблера, пакет демонстрирует высокую производительность для различных платформ (UNIX, Macintosh, PC и др.). PARI можно найти на www.parigp-home.de. ■HV: X; ., л: ■;>Й
Литература [Adam] Adams, Carlisle, Steve Lloyd: Understanding Public Key Infrastructure Concepts, Standards & Deployment, Macmillan Technical Publishing, Indianapolis, 1999. [BaSh] Bach, Eric, Jeffrey Shallit: Algorithmic Number Theory, Vol. 1, Efficient Algorithms, MIT Press, Cambridge (MA), London, 1996. [BCGP] Beauchemin, Pierre, Gilles Brassard, Claude Crepeau, Claude Goutier, Carl Pom- erance: The Generation of Random Numbers that are Probably Prime, Journal of Cryptology, Vol. 1, No. 1, pp. 53-64, 1988. [Beut] Beutelspacher, Albrecht: Kryptologie, 2. Auflage, Vieweg, 1991. [Bies] Bieser, Wendelin, Heinrich Kersten: Elektronisch unterschreiben - die digitale Signatur in der Praxis, 2. Auflage, Hiithig, 1999. [BiSh] Biham, Eli, Adi Shamir: Differential cryptanalysis of DES-like cryptosystems, Journal of Cryptology, Vol. 4, No. 1, 1991, S. 3-72. [Blum] Blum, L., M. Blum, M. Shub: A Simple Unpredictable Pseudo-Random Number Generator, SIAM Journal on Computing, Vol. 15, No. 2, 1986, S. 364-383. [BMBF] Bundesministerium fiir Bildung, Wissenschaft, Forschung und Technologie (Hrsg.): IuKDG - Informations- und Kommunikationsdienste-Gesetz-Umsetzung und Evaluierung-, Bonn, 1997. [BMWT] Bundesministerium fiir Wirtschaft und Technologie: Entwurf eines Gesetzes iiber Rahmenbedingungen fiir elektronische Signaturen - Diskussionsentwurf zur Anhorung und Unterrichtung der beteiligten Fachkreise und Verbande, April 2000, [Bone] Boneh, Dan: Twenty Years of Attacks on the RSA-Cryptosystem, Proc. ECC 1998. [Bosl] Bosch, Karl: Elementare Einfiihrung in die Wahrscheinlichkeitsrechnung, Vieweg, 1984. [Bos2] Bosch, Karl: Elementare Einfiihrung in die angewandte Statistik, Vieweg, 1984. [Boss] Bosselaers, Antoon, Rene Govaerts, Joos Vandewalle: Comparison of three modular reduction functions, in: Advances in Cryptology, CRYPTO 93, Lecture Notes in Computer Science 773, S. 175-186, Springer-Verlag, New York, 1994. [Bres] Bressoud, David M.: Factorization and Primality Testing, Springer-Verlag, New York, 1989. [BSI1] Bundesamt fur Sicherheit in der Informationstechnik: Geeignete Kryptoalgorithmen. In Erfullung der Anforderungen nach §17 (1) SigG vom 16. Mai 2001 in Verbindung mit §17(2) SigV vom 22. Oktober 1997. In: Bundesanzeiger 158 (2001). S. 18.562. [BSI2] Bundesamt fiir Sicherheit in der Informationstechnik: Anwendungshinweise und Interpretation zum Schema (AIS). Funktionalitatsklassen und Evaluationsmethodo- logiefiir physikalische Zufallszahlengeneratoren. AIS 31. Version 1. Bonn, 2001. [Burt] Burthe, R. J. Jr.: Further Investigations with the Strong Probable Prime Test, Mathematics of Computation, Volume 65, pp. 373-381, 1996.
450. Криптография на Си и C++ в действии [Bund] Bundschuh, Peter: Einfiihrung in die Zahlentheorie, 3. Auflage, Springer-Verlag, Berlin, Heidelberg, 1996. [BuZi] Burnikel, Christoph, Joachim Ziegler: Fast recursive Division, Forschungsbericht MPI-I-98-1-022, Max-Planck-Institut fur Informatik, Saarbrucken, 1998. [Char] Chad, Suresh, Charanjit Jutla, Josyula R. Rao, Pankaj Rohatgi: A Cautionary Note Regarding Evaluation of AES Candidates on Smart Cards, http://csrc.nist.gov/encryp- tion/aes/roundl/conf2/papers/chari.pdf, 1999. [Cohe] Cohen, Henri: A Course in Computational Algebraic Number Theory, Springer- Verlag, Berlin, Heidelberg, 1993. [Coro] Coron, Jean-Sebastien, David Naccache, Julien P. Stern: On the Security of RSA Padding, in M. Wiener (Ed.), Advances in Cryptology, CRYPTO '99, Lecture Notes in Computer Science No. 1666, S. 1-17, Springer-Verlag, New York, 1999. [Cowi] Cowie, James, Bruce Dodson, R.-Marije Elkenbracht-Huizing, Arjen K. Lenstra, * Peter L. Montgomery, Joerg Zayer: A world wide number field sieve factoring re- v„a;.. cord: on to 512 bits, in K. Kim and T, Matsumoto (Ed.) Advances in Cryptology, ASIACRYPT '96, Lecture Notes in Computer Science No. 1163, pp. 382-394, Springer-Verlag, Berlin 1996. [DaLP] Damgard, Ivan, Peter Landrock, Carl Pomerance: Average Case Error Estimates for the Strong Probable Prime Test, Mathematics of Computation, Volume 61, pp. 177-194, 1993. [DaRi] Daemen, Joan, Vincent Rijmen: AES-Proposal: Rijndael, Doc. Vers. 2.0, http://www.nist.gov/encryption/aes, Sept. 1999. [DR02] Daemen, Joan, Vincent Rijmen: The Design of RijndaeL: AES - The Advanced Encryption Standard, Springer-Verlag, Heidelberg, 2002. [Deit] Deitel, H. M., P. J. Deitel: C++ How To Program, Prentice Hall, 1994. [Dene] Denert, Ernst: Software-Engineering, Springer-Verlag, Heidelberg, 1991. [deWe] de Weger, Benne: Cryptanalysis of RSA with small prime difference, Cryptology ePrint Archive, Report 2000/016, 2000. [Diff] Diffie, Whitfield, Martin E. Hellman: New Directions in Cryptography, IEEE Trans. Information Theory, S. 644-654, Vol. IT-22, 1976. [DoBP] Dobbertin, Hans, Antoon Bosselaers, Bart Preneel: RIPEMD-160, a strengthened version of RIPEMD, in D. Gollman (Hrsg.): Fast Software Encryption, Third International Workshop, Lecture Notes in Computer Science 1039, S. 71-82, Springer- Verlag, Berlin, Heidelberg, 1996. [DuKa] Dusse, Stephen R., Burton. S. Kaliski: A Cryptographic Library for the Motorola DSP56000, in: Advances in Cryptology, EUROCRYPT '90, Lecture Notes in Computer Science No. 473, S. 230-244, Springer-Verlag, New York, 1990. [Dune] Duncan, Ray: Advanced OS/2-Programming: The Microsoft Guide to the OS/2-kernel for assembly language and С programmers, Microsoft Press, Redmond, Washington, 1981.
Литература ■';■-■ ■■ : : ■ У 451 [East] Eastlake, D., S. Crocker, J. Schiller: Randomness Recommendations for Security, RFC1750, 1994. [Elli] Ellis, James H.: The Possibility of Secure Non-Secret Digital Encryption, 1970, http://www.cesg.gov.uk/about/nsecret/home.htm [ElSt] Ellis, Margaret A., Bjarne Stroustrup: The Annotated C++ Reference Manual, Addi- son-Wesley, Reading, MA, 1990. [Endl] Endl, Kurth, Wolfgang Luh: Analysis I, Akademische Verlagsgesellschaft Wiesbaden, 1977. [Enge] Engel-Flechsig, Stefan, Alexander RoBnagel (Hrsg.): Multimedia-Recht, С. H. Beck, Munchen, 1998. [EESSI] European Electronic Signature Standardization Initiative: Algorithms and Parameters for Secure Electronic Signatures, V.1.44 DRAFT, 2001. [EU99] Richtlinie 1999/93/EG des Europaischen Parlaments und des Rates vom 13. Dezem- ber 1999 uber gemeinschaftlkhe Rahmenbedingungen fur etektronische Signatures [Evan] Evans, David: LCLint Users Guide, Version 2.4, MIT Laboratory for Computer Science, April 1998. [Fegh] Feghhi, Jalal, Jalil Feghhi, Peter Williams: Digital Certificates: Applied Internet Security, Addison-Wesley, Reading, MA, 1999. [Fiat] Fiat, Amos, Adi Shamir: How to prove yourself: Practical Solutions to Identification and Signature Problems, in: Advances in Cryptology, CRYPTO '86, Lecture Notes in Computer Science 263, S. 186-194, Springer-Verlag, New York, 1987. [FIPS] Federal Information Processing Standard Publication 140 - 1: Security requirements for cryptographic modules, US Department of Commerce/ National Institute of Standards and Technology (NIST), 1992. [FI81] National Institute of Standards and Technology: DES Modes of Operation, Federal Information Processing Standard 81, NIST, 1980. [F197] National Institute of Standards and Technology: ADVANCED ENCRYPTION STANDARD (AES), Federal Information Processing Standards Publication 197, November 26, 2001 [Fisc] Fischer, Gerd, Reinhard Sacher: Einfiihrung in die Algebra, Teubner, 1974. [Fors] Forster, Otto: Algorithmische Zahlenthorie, Vieweg, Braunschweig,1996. [Fumy] Fumy, Walter, Hans Peter RieB: Kryptographie, 2. Auflage, Oldenbourg, 1994. [Gimp] Gimpel Software: PC-lint, A Diagnostic Facility for С and C++. [Glad] Glade, Albert, Helmut Reimer, Bruno Struif (Hrsg.): Digitale Signatur & Sicherheits- sensitive Anwendungen, DuD-Fachbeitrage, Vieweg, 1995. [Gldm] Gladman, Brian: A Specification for Rijndael, the AES Algorithm, http://fp.glad- man.plus.com, 2001.
452 Криптография на Си и C++ в действии [GoPa] Goubin, Louis, Jacques Patarin: DES and Differential Power Analysis, in Proceedings of CHES '99, Lecture Notes in Computer Science, Vol. 1717, Springer-Verlag, 1999. [Gord] Gordon, J. A.: Strong Primes are Easy to Find, Advances in Cryptology, Proceedings of Eurocrypt "84, S. 216-223, Springer-Verlag, Berlin, Heidelberg, 1985. [Halm] Halmos, Paul, R.: Naive Mengenlehre, 3. Auflage, Vandenhoeck & Ruprecht, Got- tingen, 1972. [Harb] Harbison, Samuel P, Guy L. Steele jr.: C, a reference manual, 4th Edition, Prentice Hall, Englewood Cliffs, 1995. [Hatt] Hatton, Les: Safer C: Developing Software for High-integrity and Safety-critical Systems, McGraw-Hill, London, 1995. [Henr] Henricson, Mats, Erik Nyquist: Industrial Strength C++, Prentice Hall, New Jersey, 1997. [Heid] Heider, Franz-Peter: Quadratische Kongruenzen, unveroffentlichtes Manuskript, Koln, 1997. [HeQu] Heise, Werner, Pasquale Quattrocchi: Informations- und Codierungstheorie, Springer- Verlag, Berlin, Heidelberg, 1983. [HKW] Heider, Franz-Peter, Detlef Kraus, Michael Welschenbach: Mathematische Metho- den der Kryptoanalyse, DuD-Fachbeitrage, Vieweg, Braunschweig, 1985. [Herk] Herkommer, Mark: Number Theory, A Programmers Guide, McGraw-Hill, 1999. [HoLe] Howard, Michael, David LeBlanc: Writing Secure Code, Microsoft Press, 2002. [IEEE] IEEE P1363 / D13: Standard Specifications For Public Key Cryptography, Draft Version 13, November 1999. [ISOl] ISO/IEC 10118-3: Information Technology - Security Techniques - Hash-functions- Part 3: Dedicated hash-functions, CD, 1996. [IS02] ISO/IEC 9796: Information Technology - Security Techniques - Digital signature scheme giving message recovery, 1991. [IS03] ISO/IEC 9796-2: Information Technology - Security Techniques - Digital signature scheme giving message recovery, Part 2: Mechanisms using a hash-function, 1997. [Koeu] Koeune, F., G. Hachez, J.-J. Quisquater: Implementation of Four AES Candidates on Two Smart Cards, UCL Crypto Group, 2000. [Knut] Knuth, Donald Ervin: The Art of Computer Programming, Vol. 2: Seminumerical Algorithms, 3rd Edition, Addison-Wesley, Reading, MA, 1998. [Kobl] Koblitz, Neal: A course in number theory and cryptography, Springer-Verlag, New York, 2nd Edition 1994. [Kob2] Koblitz, Neal: Algebraic Aspects of cryptography, Springer-Verlag, Berlin, Heidelberg, 1998. [KoJJ] Kocher, Paul, Joshua Jaffe, Benjamin Jun: Introduction to Differential Power Analysis and Related Attacks, 1998, http://www.cryptography.com/dpa/technical/
Литература 453 [Kran] Kranakis, Evangelos: Primality and Cryptography, Wiley-Teubner Series in Computer Science, 1986. [KSch] Kuhlins, Stefan, Martin Schader: Die C++-Standardbibliothek, Springer-Verlag, Berlin, 1999. [LeVe] Lenstra, Arjen K., Eric R. Verheul: Selecting Cryptographic Key Sizes, www.crypto- savvy.com, 1999. [Lind] van der Linden, Peter: Expert С Programming, SunSoft/Prentice Hall, Mountain View, CA, 1994. [Lipp] Lippman, Stanley, В.: C++ Primer, 2nd Edition, Addison-Wesley, Reading, MA, 1993. [Magu] Maguire, Stephen A.: Writing Solid Code, Microsoft Press, Redmond, Washington, 1993. [Matt] Matthews, Tim: Suggestions for Random Number Generation in Software, RSA Data Security Engineering Report, December 1995. [Mene] Menezes, Alfred J.: Elliptic Curve Public Key Cryptosystems, Kluwer Academic Publishers, 1993. [Mess] Messerges, Thomas S.: Securing the AES Finalists Against Power Analysis Attacks, Fast Software Encryption Workshop 2000, Lecture Notes in Computer Science, Springer-Verlag. [Meyl] Meyers, Scott D.: Effective C++, 2nd Edition, Addison-Wesley, Reading, Mass., 1998. [Mey2] Meyers, Scott D.: More Effective C++, 2nd Edition, Addison-Wesley, Reading, Mass., 1998. [Mied] Miedbrodt, Anja: Signaturregulierung im Rechtsvergleich. Ein Vergleich der Regu- lierungskonzepte in Deutschland, Europa und den Vereinigten Staaten von Amerika, Der Elektronische Rechtsverkehr 1, Nomos Verlagsgesellschaft, Baden-Baden, 2000. [MOV] Menezes, Alfred J., Paul van Oorschot, Scott A. Vanstone, Handbook of Applied Cryptography, CRC Press, 1997. [Mont] Montgomery, Peter L.: Modular Multiplication without Trial Division, Mathematics of Computation, S. 519-521,44 (170), 1985. [Murp] Murphy, Mark L.: C/C++ Software Quality Tools, Prentice Hall, New Jersey, 1996. [Nied] Niederreiter, Harald: Random Number Generation and Quasi-Monte Carlo Methods, SIAM, Philadelphia, 1992. [NIST] Nechvatal, James, Elaine Barker, Lawrence Bassham, William Burr, Morris Dworkin, James Foti, Edward Roback: Report on the Development of the Advanced Encryption Standard, National Institute of Standards and Technology, 2000. [Nive] Niven, Ivan, Herbert S. Zuckerman: Einfiihrung in die Zahlentheorie Bd. I und II, Bibliographisches Institut, Mannheim, 1972.
454 Криптография на Си и C++ в действии [Odly] Odlyzko, Andrew: Discrete logarithms: The past and the future, AT&T Labs Research, 1999. [Petz] Petzold, Charles: Programming Windows: The Microsoft Guide to Writing Applications for Windows 3.1, Microsoft Press, Redmond, Washington, 1992, [Plal] Plauger, P. J.: The Standard С Library, Prentice-Hall, Englewood Cliffs, New Jersey, 1992. [Pla2] Plauger, P. J.: The Draft Standard C++ Library, Prentice-Hall, Englewood Cliffs, New Jersey, 1995. [Pren] Preneel, Bart: Analysis and Design of Cryptographic Hash Functions, Dissertation an der Katholieke Universiteit Leuven, 1993. [Rabi] Rabin, Michael, O.: Digital Signatures and Public-Key Functions as Intractable as Factorization, MIT Laboratory for Computer Science, Technical Report, MIT/LCS/TR-212, 1979. [RDS1] RSA Data Security, Inc.: Public Key Cryptography Standards, PKCS #1: RSA Encryption, RSA Laboratories Technical Note, Version 2.0, 1998. [RDS2] RSA Security, Inc.: Recent Results on Signature Forgery, RSA Laboratories Bulletin, 1999, http://www.rsasecurity.com/rsalabs/html/sigforge.html . [RegT] Regulierungsbehorde fiir Telekommunikation und Post (RegTP): Bekanntmachung zur digitalen Signatur nach Signaturgesetz und Signaturverordnung, Bundesanzeiger Nr. 31,14.02.1998. [Rein] Reinhold, Arnold: P=?NP Doesn't Affect Cryptography, http://world.std.com/~rein- hold/p=np.txt, Mai 1996. [Ries] Riesel, Hans: Prime Numbers and Computer Methods for Factorization, Birkhauser, Boston, 1994. [Rive] Rivest, Ronald, Adi Shamir, Leonard Adleman: A Method for Obtaining Digital Signatures, Communications of the ACM 21, S. 120-126, 1978. [Rose] Rose, H: E.: A course in number theory, 2nd Edition, Oxford University Press, Oxford, 1994. [Saga] Sagan, Carl: Cosmos, Random House, New York, 1980. [Salo] Salomaa, Arto: Public-Key Cryptography, 2nd Edition, Springer-Verlag, Berlin, Heidelberg, 1996. [Schn] Schneier, Bruce: Applied Cryptography, 2nd Edition, John Wiley & sons, New York, 1996. [Scho] Schonhage, Arnold: A Lower Bound on the Length of Addition Chains, Theoretical Computer Science, S. 229-242, Vol. 1, 1975. [Schr] Schroder, Manfred R.: Number Theory in Science and Communications, 3rd ed., Springer-Verlag, Berlin, Heidelberg, 1997. [SigG] Gesetz uber Rahmenbedingungen fiir elektronische Signaturen und zur Anderung weiterer Vorschriften, unter http://www.iid.de/iukdg, 2001.
Литература 455 [SigV] Verordnung zur elektronischen Signatur (Signaturverordnung - SigV) vom 16. November 2001. [Skal] Skaller, John Maxwell: Multiple Precision Arithmetic in C, in: Schumacher, Dale (Editor): Software Solutions in C, Academic Press, S. 343-454, 1994. [Spul] Spuler, David A.: C++ and С Debugging, Testing and Reliability, Prentice Hall, New Jersey, 1994. [Squa] Daemen, Joan, Lars Knudsen, Vincent Rijmen: The Block Cipher Square, Fast Software Encryption, Lecture Notes in Computer Science 1267, Springer-Verlag, 1997, S,149-165. [Stal] Stallings, William.: Cryptography and Network Security, 2nd Edition, Prentice Hall, New Jersey, 1999. [Stin] Stinson, Douglas R,: Cryptography - Theory and Practice, Prentice Hall, New Jersey, 1995. [Stlm] Stallman, Richard M.: Using and Porting GNU CC, Free Software Foundation. [Strl] Stroustrup, Bjarne: The C++ Programming Language, 3rd Edition, Addison-Wesley, Reading, MA, 1997. [Str2] Stroustrup, Bjarne: The Design and Evolution of C++, Addison-Wesley, Reading, MA, 1994. [Teal] Teale, Steve: C++ IOStreams Handbook, Addison-Wesley, Reading, MA, 1993. [Wien] Wiener, Michael: Cryptanalysis of short RSA secret exponents, in: IEEE Transactions on Information Theory, 36(3): S. 553-558, 1990. [Yaco] Yacobi, Y.: Exponentiating faster with Addition Chains, in: Advances in Cryptology, EUROCRYPT '90, Lecture Notes in Computer Science 473, S. 222-229, Springer- Verlag, New York, 1990. [Zieg] Ziegler, Joachim: Personliche Kommunikation 1998, 1999.
Содержание U <*.,...- i „..„,.... ,;.....,..':„ Предисловие к русскому изданию 6 jM*.*f**< ^ Предисловие ко второму изданию 7 Предисловие к первому изданию 9 ЧАСТЬ I. АРИФМЕТИКА И ТЕОРИЯ ЧИСЕА НА С ГААВА 1. Введение 15 1.1. О программном обеспечении 19 1.2. Законные условия использования программного обеспечения 23 1.3. Как связаться с автором 23 ГААВА 2. Числовые форматы: представление больших чисел в языке С 25 ГААВА 3. Семантика интерфейса 31 ГААВА 4. Основные операции 35 4.1. Сложение и вычитание 36 4.2. Умножение 46 4.2.1. Школьный метод 47 4.2.2. А возведение в квадрат - быстрее 54 4.2.3. Поможет ли метод Карацубы? 59 4.3. Деление с остатком 64 ГААВА 5. Модульная арифметика: вычисление в классах вычетов 81 ГААВА 6. Все дороги ведут к... модульному возведению в степень 95 6.1. Первые шаги 95
458 Криптография на Си и C++ в действии j •$$,*■« л„ и*+****•*• 6.2. М-арное возведение в степень 101 63. Аддитивные цепочки и окна 11$ 6.4. Приведение по модулю и возведение в степень методом Монтгомери 123 6.5. Криптографические приложения ■ модульного возведения в степень 136 Й,$; ** ,. те w,JAA$AZ*o* Поразрядные и логические функции 143 7.1. Операции сдвига 143 7.2. Все или ничего: битовые соотношения 150 7.3. Прямой доступ к отдельным двоичным разрядам 156 7.4. Операции сравнения 160 [ ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 165 ГЛАВА 9. Динамические регистры 177 j ГЛАВА 10. Основные теоретико-числовые функции 187 10.1. Наибольший общий делитель 188 -W*'- 10.2. Обращение в кольце классов вычетов 196 10.3. Корни и логарифмы 205 10.4. Квадратные корни в кольце классов вычетов 211 '„, ;,„^ 10.4.1. Символ Якоби 212 10.4.2. Квадратные корни по модулю/?* 220 10.4.3. Квадратные корни по модулю п 225 10.4.4. Квадратичные вычеты в криптографии 234 10.5. Проверка на простоту 237 ГЛАВА 11. Большие случайные числа 257 ГЛАВА 12. Стратегия тестирования LINT 271 12.1. Статический анализ 273 12.2. Динамические тесты 27э *## я ****
Содержание 459 ЧАСТЬ II. КЛАСС LINT: АРИФМЕТИКА НА C++ ГЛАВА 13. Пусть C++ облегчит Вашу жизнь 285 ,. 13.1. Частное дело: представление чисел в классе LINT 291 ■ 13.2. Конструкторы 293 13.3. Перегрузка операторов 297 ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 305 14.1. Арифметика 305 14.2. Теория чисел 315 14.3. Потоковый ввод/вывод объектов LINT 320 14.3.1. Форматированный вывод объектов LINT 321 14.3.2. Манипуляторы 329 14.3.3. Файловый ввод/вывод для объектов LINT 332 ГЛАВА 15. Обработка ошибок 339 15.1. (Без) Паники 339 15.2. Обработка ошибок, определяемая пользователем 342 15.3. Исключения LINT 343 ГЛАВА 16. Практический пример: криптосистема RSA...351 16.1. Асимметричные криптосистемы 352 ' 16.2. Алгоритм RSA 354 16.3. Цифровая подпись RSA 369 16.4. RSA-классы на C++ 377 ГЛАВА 17. Сделайте это сами: протестируйте LINT 387 ГЛАВА 18. Направления дальнейших исследований 391 ГЛАВА 19. Rijndael: наследник стандарта шифрования данных 393 * 19.1. Полиномиальная арифметика 395 ""•""" "" 19.2. Алгоритм Rijndael 400
460 Криптография на Си и C++ в действии 19.3. Вычисление ключа раунда 403 19.4. S-блок 405 19.5. Преобразование ShiftRow 407 кЬ,+ >.--~^,> .*пй j J9.6. Преобразование MixCoIumn 408 ^ --'->'—v— 19 7. Сложеннее ключом раунда , 409 Н'Х-.■'■'""" ' " 19-8- Полная процедура зашифрования блока 409 СУУША 7, 19.9. Расшифрование 413 £Л* v. ' ■ ЧАСТЬ III. ПРИЛОЖЕНИЯ Приложение А. Каталог функций на С 421 АЛ Ввод/вывод, присваивание, преобразования, сравнения 421 А2 Основные арифметические операции 422 A3 Модульная арифметика 422 А.4 Битовые операции 424 ^ -**.. А.5 Теоретико-числовые функции 424 £ i"- $lf t ^ьу A.6 Генерация псевдослучайных чисел 425 ;,{ ...,...,. А.7 Управление регистрами 426 Приложение В. Каталог функций C++ 427| 8.1 Ввод/вывод, преобразования, сравнения: функции-члены класса 427| 8.2 Ввод/вывод, преобразования, сравнения: функции-друзья класса 429| В.З Основные операции: функции-члены класса 430 *"*■' В,4 Основные операции: функции-друзья класса 4311 8.5 Модульная арифметика: функции-члены класса 432 8.6 Модульная арифметика: функции-друзья класса 433 8.7 Битовые операции: функции-члены класса 433 :\ 12* В.8 Битовые операции: функции-друзья класса 434 сое В.9 Теоретико-числовые функции-члены класса 434 ВЛ0 Теоретико-числовые функции-друзья класса 435 В.11 Генерация псевдослучайных чисел 438 W .__.,. ■-.. ..ft В. 12 Прочие функции 4:>г>
Содержание 461 Приложение С. Макросы 439 С. 1 Коды ошибок и значения состояний 439 С.2 Дополнительные константы 439 С.З Макросы с параметрами 440 Приложение D. Время вычислений 443 Приложение Е. Условные обозначения 445 Приложение F. Арифметические и теоретико-числовые пакеты 447 Литература 449 Об авторе 456 1