Текст
                    УДК 004.43
М. Вельшенбах
Криптография на Си и C++ в действии. Учебное пособие.—
М.: Издательство Триумф, 2004 — 464 с.: ил.
ISBN 5-89392-083-Х
ISBN 3-54042061-4 (нем.)
Несмотря на то, что настоящее издание содержит математическую
теорию новейших криптографических алгоритмов, книга в большей степени
рассчитана на программистов-практиков. Здесь Вы найдете описание
особенностей эффективной реализации криптографических алгоритмов на
языках Си и C++, а также большое количество хорошо документированных
исходных кодов, которые записаны на компакт-диск, прилагаемый к книге.
Купите книгу, и Вы легко сможете снабдить свои собственные
программные разработки сильной криптографической защитой.
Originally published in German.
Kryptographie in C und 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-X
ISBN 3-54042061-4 (нем.)
© Обложка, серия, оформление
ООО «Издательство ТРИУМФ», 2004

Fiir 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 Stricker из издательства Springer-Verlag за безграничное доверие и сотрудничество. Я глубоко благодарен моему американскому переводчику Дэвиду Крамеру (David Kramer), компетентно и самоотверженно потру- дившемуся и давшему множество ценных советов, которые были использованы и в немецком издании этой книги. Предупреждение Прежде чем где-либо использовать программы, содержащиеся в этой книге, внимательно прочитайте руководство пользователя и техническое описание соответствующего программного обеспече- ния и компьютера. Ни автор, ни издательство не несут ответствен- ности за ущерб, вызванный некорректным выполнением инструк- ций и программ, содержащихся в этой книге, или ошибками в тек- сте или в программах, которые, несмотря на тщательную проверку, все же могут остаться. Программы, содержащиеся на CD-ROM, защищены авторскими правами и не могут быть воспроизведены без разрешения издательства.
Предисловие к первому изданию '.н д) 1ШР: . ) <" я \/Н'. ~t< ж. wUr’-r, п > |{1 .п.-. . . < '.С’ЬН R04 him/МП Хл < ’ • '< У'> 4'Иh'j ‘ - " h ,• i i d 'I s ’ Г 'i t 'aux пр F4 ад • .‘dW . » : '>'.)'?/ ь ч < / •I, ЧЧ Ц, V •. * ч '< .,uu - 1 л +*!М -f)5>q ИЬ '.ЧЧ { • X.1 ХЦНММ ‘ « /Л. и lb Ow*! Математика - царица наук, теория чисел - царица математики. Иногда она снисходит до того, чтобы помочь астрономии и другим есте- ственным наукам, но при любых обстоятельст- вах она - первая. Карл Фридрих Гаусс Зачем нужна книга по криптографии, посвященная в основном арифметике целых чисел и ее реализации в виде компьютерных программ? Насколько это важно по сравнению с теми большими задачами, которые решает программист? Если ограничиться лишь теми числами, которые могут быть описаны стандартными число- выми типами какого-либо языка программирования, арифметика будет делом довольно легким: обычные арифметические операции задаются в программах обычными символами +, Но как только нам нужны результаты, длина которых намного больше 16 или 32 битов, все становится гораздо интересней. Для таких чисел даже простые арифметические операции уже не годятся, и приходится потрудиться над разрешением таких проблем, кото- рые раньше и проблемами-то не казались. С этим сталкивается лю- бой, будь то профессионал или любитель, кто занимается теорией чисел: пытаясь применить школьные алгоритмы арифметики, мы вдруг оказываемся втянутыми в невероятно запутанный процесс. Читатель, который собирается разрабатывать программы в этой об- ласти и не желает изобретать колесо, найдет в этой книге целый ряд функций, оперирующих с большими числами, на языках С и C++. Речь идет отнюдь не об «игрушечных» примерах, поясняющих «как это работает в принципе», но о готовом пакете функций и методов, удовлетворяющих профессиональным требованиям в части кор- ректности, быстродействия и серьезной теоретической базы. Цель этой книги - связать теорию и практику, перекинуть мост че- рез пропасть, разделяющую теоретическую литературу и практиче- ские задачи программирования. Последовательно, шаг за шагом мы будем познавать фундаментальные принципы арифметики больших натуральных чисел, арифметики конечных колец и полей, сложные функции элементарной теории чисел, что позволит пролить свет на многочисленные и разнообразные возможности применения этих принципов в современной криптографии. Сведения из математики приводятся здесь в объеме, необходимом для понимания пред- ставленных программ; более глубокие знания можно почерпнуть из обширного списка литературы. Все разработанные нами функ- ции постепенно объединяются и многократно тестируются, так что в итоге мы получаем полезный объемлющий программный интерфейс.
10 Криптография на Си и C++ в действии Мы начинаем с представления больших чисел и с изучения основ- ных вычислительных операций, создавая для сложения, вычитания, умножения и деления больших чисел мощные базовые функции. Исходя из этого, мы поясняем модульную арифметику в классах вычетов и реализуем соответствующие операции в виде библиотеч- ных функций. Отдельная глава посвящена трудоемкому процессу возведения в степень, те разрабатываются и программируются различные специальные алгоритмы модульной арифметики. После тщательной подготовки, включающей в себя также ввод и вывод больших чисел и их преобразование в различных системах счисления, мы рассматриваем элементарные теоретико-числовые алгоритмы, используя для этого базовые арифметические операции, а затем разрабатываем программы, начиная с вычисления наиболь- шего общего делителя больших чисел. Следом идут такие задачи, как вычисление символов Лежандра и Якоби, обращение и возве- дение в квадрат в конечных кольцах. Мы знакомимся также с ки- тайской теоремой об остатках и ее приложениями. Попутно мы несколько подробнее останавливаемся на принципах распознавания больших простых чисел и программируем мощный тест простоты. Следующая глава посвящена генерации больших случайных чисел, разработке и проверке статистических свойств криптографически стойкого генератора случайных битов. Завершается первая часть тестированием арифметических и других функций. Для этого, исходя из математических правил арифметики, мы разрабатываем специальные методы проверки, а также обсуж- даем реализацию эффективных внешних средств. Во второй части мы шаг за шагом строим класс 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. Введение Целые числа сотворил Бог. Все остальное - дело рук человеческих. г . ( , Леопольд Кронекер • С1' >г,к о ’ Если вы посмотрите на нуль, то не увидите ни- / ’ чего; но взгляните сквозь него - и вы увидите \ • МИР- Роберт Каплан, Естественная история Нуля к’ ! ". В наши дни занятие криптографией волей-неволей влечет за собой углубленное изучение теории чисел, а именно, изучение нату- ,, . * ральных чисел, которые представляют одну из занимательнейших > , областей математики. Однако мы не станем уподобляться глубоко- .. и, , водным ныряльщикам, чтобы добыть со дна математического океа- ,, и,-. на затонувшие сокровища, - для криптографических приложений ч л ,rj это не требуется. Наша цель намного скромнее. С другой стороны, внедрение теории чисел в криптографию беспредельно глубоко, и ; этому способствовали многие выдающиеся математики. - Теория чисел уходит корнями в античность. Уже в VI веке до н. э. ’ ‘ пифагорейцы (греческий математик и философ Пифагор и его ; школа) серьёзно занимались целыми числами и достигли значи- ' • ' тельных математических результатов, например, таких как знаме- нитая теорема Пифагора, известная сейчас каждому школьнику. ‘ С религиозным рвением они утверждали, что все числа соотносятся ; с натуральными числами, и оказались перед серьёзной дилеммой, когда открыли существование иррациональных чисел, таких как ? . V2 , которые нельзя представить в виде частного двух целых. Это м .. < , . • открытие до такой степени не укладывалось в представление пифа- а ,г горейцев о мире, что они избрали достойную сожаления тактику ' * ' ’ г. . поведения, часто повторяющуюся в истории человечества, - поста- < t : ... рались утаить знание об иррациональных числах. От греческих математиков Евклида (III век до н.э.) и Эратосфена . г Д' (276-195 гг. до н.э.) до нас дошли два древнейших алгоритма тео- г" ’ ‘ рии чисел. Они тесно связаны с самыми современными алгоритма- ми шифрования, которые мы используем повседневно для надеж- ной связи через Интернет. Алгоритм Евклида и «решето» Эратос- фена полностью отвечают целям нашей работы, и мы рассмотрим их теорию и применение в пп. 10.1 и 10.5 данной книги.
16 Криптография на Си и C++ в действии » t :.i;..fh * 5 • 0 ГМ' К числу главных основоположников современной теории чисел от- носятся Пьер Ферма (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) Последующие элементы двух неравных натуральных чисел неравны: Из п т следует, что м+ т+ для всех п, те IN. (II) Каждое натуральное число, за исключением нуля, имеет предшест- венника: IN+ = IN \{ 0}. (III) Принцип полной индукции'. Если S с IN, 0 6 S, и из п G S всегда сле- дует, что п G S , то S = IN. Принцип полной индукции позволяет вывести интересующие нас арифметические операции над целыми числами. Фундаментальные операции сложения и умножения можно вывести рекурсивно сле- дующим образом. Начнем со сложения: Для каждого натурального числа и е IN существует функция sn: IN —> IN, такая, что (a) sn(0) = n (б) 5,/хЭ = (^п(х))+ для всех натуральных чисел х е IN. Значение функции sn(x) называется суммой п + х чисел них. Однако существование таких функций sn для всех натуральных чисел п е IN требуется доказать, так как бесконечно большое ко- личество натуральных чисел не оправдывает априори такого допу- щения. Для доказательства следует вернуться к принципу полной индукции в соответствии с вышеупомянутой третьей аксиомой Пеано (см. [Halm], главы 11-13). Операция умножения выводится аналогичным образом: 1 При этом не имеет значения тот факт, что в соответствии со стандартом DIN 5473 нуль не отно- сится к натуральным числам. В информатике, как правило, принято начинать счет с 0, а не с 1. Это указывает на важную роль нуля как нейтрального элемента при сложении (аддитивное тождество).
18 Криптография на Си и C++ в действии Для каждого натурального числа п е П\| существует функция рп: IN —» IN, такая, что (а) Рп^ = 0 ' (б) - л*, ‘ - . ' рп(х+) - рп(х) + п для всех натуральных чисел х е IN. Значение функции рп(х) называется произведением п • х чисел п и х. Как и следовало ожидать, умножение определено в терминах сло- жения. Для арифметических операций, определенных таким обра- зом, можно доказать, применяя повторно полную индукцию от х в соответствии с Аксиомой III, такие известные арифметические за- коны, как ассоциативность, коммутативность и дистрибутивность (см. [Halm], глава 13). Хотя обычно мы используем эти законы без всяких церемоний, оговоримся, что будем очень часто обращаться к их помощи при тестировании FLINT-функций (см. главы 12 и 17). Подобным же образом получаем определение возведения в сте- пень. Мы приведем его здесь ввиду важности этой операции в дальнейшем. Для каждого натурального числа п G IN существует функция еп: IN —> IN, такая, что (а) ^(0)=1 (б) г 4- ен(х+) = еп(х) • п для всех натуральных чисел х g IN. Значение функции еп(х) называется степенью х пх числа п. Используя полную индукцию, можно доказать правила возведения в степень пх- пу = пх+у,пх- mx=(ir т)х, (пх)у = пху, к которым мы вернемся в главе 6. В дополнение к вычислительным операциям, на множестве IN нату- • г; --г -•. ральных чисел определено отношение порядка «<», позволяющее сравнивать два элемента н, те IN. Несмотря на важность этого факта в теории множеств, здесь мы отметим лишь, что отношение порядка обладает точно теми же свойствами, которые мы знаем и используем в нашей повседневной жизни. Начав с установления пустого множества в качестве единственного фундаментального блока для построения натуральных чисел, при- ступим теперь к изучению материалов, с которыми нам предстоит работать в дальнейшем. Теория чисел большей частью рассматри- вает натуральные и целые числа как данные и с ходу приступает
ГЛАВА 1. Введение 19 к изучению их свойств. Тем не менее, нам интересно хотя бы раз взглянуть на процесс «математического клеточного деления» - процесс, выдающий в результате не только натуральные числа, но также арифметические операции и правила, с которыми мы будем очень тесно взаимодействовать. 1.1.0 программном обеспечении ; Программное обеспечение, описанное в этой книге, в целом пред- ставляет собой пакет, так называемую библиотеку функций, к ко- торым часто обращаются. Название этой библиотеки - FLINT/C - ' * является аббревиатурой для «Functions for Large Integers in Number Theory and Cryptography» (функции для больших целых в теории чисел и криптографии). FLINT/C содержит, среди всего прочего, следующие модули, кото- рые можно найти в виде кода (текста программы) на сопроводи- ** ' м тельном CD-ROM. / J j Таблииа 1.1. flint.h Заголовочный файл для использования Арифметика и функиий из flint.c теория чисел на С в директории flint/src flint.c Функции арифметики и теории чисел на языке С kmul.h,c Функции для умножения и возведения ripemd.h/C в квадрат по методу Каранубы Реализация хэш-функции RIPEMD-160 Таблииа 1.2. Арифметика и теория чисел на С++ в директории flint/src flintpp.h flintpp.cpp 'u, Заголовочный файл для использования функций из flintpp.cpp Функции арифметики и теории чисел на языке C++. Этот модуль использует функции из flint.c Таблииа 1.3. Арифметический модуль на Ассемблере 80x86 (см. главу 18) в директории flint/src/asm mult.asm umul.asm sqr.asm div.asm Умножение, заменяет С-функнию mult() из flint.c Умножение, заменяет С-функиию umul() Возведение в квадрат, заменяет С-функиию sqr() Деление, заменяет С-функнию div_l()
20 Криптография на Си и C++ в действии Таблииа 1.4. Арифметические библиотеки на Ассемблере 80x86 (см. главу 18) в директории flint/lib flinta.lib fl intavc.lib flinta.a libflint.a Библиотека ассемблерных функций в формате OMF Библиотека ассемблерных функций в формате COFF Архив ассемблерных функций для emx/gcc под OS/2 ' ' ' Архив ассемблерных функций для использования под LINUX Таблииа 1.5. testxxx.clpp] Тестовые программы на С и C++ Тесты (см. п. 12.2 и главу 17) в директории flint/test Таблииа 1.6. Реализация RSA (см. главу 16) в директории flint/rsa rsakey.h Заголовочный файл для классов RSA rsakey.cpp Реализация классов RSA RSAkey и RSApub rsademo.cpp Пример применения классов 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). Ассемблерные программы можно транслировать с помощью Micro- soft MASM2 или Watcom WASM. Они содержатся в скомпилиро- ванном виде на CD-ROM в виде библиотек в форматах OMF (фор- мат объектного модуля) и COFF (общий формат объектного файла), 2 Вызов: ml /Сх /с /Gd <имя файла>
ГЛАВА#. Введение 21 соответственно, а также в виде 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. В зависимости от компьютерной платформы возможны отклонения, касающиеся опций компилятора. Но для достижения максимальной производительности всегда следует включать опции оптимизации по скорости. Из-за требований к стеку для многих операционных 4. сред и приложений он должен быть правильно настроен3. Что каса- i ется необходимого размера стека для конкретных приложений, следует отметить замечание о функциях возведения в степень в главе 6 и обзор на странице 140. Стековые требования можно сделать менее жесткими, используя функцию возведения в степень с J динамическим распределением стека, а также посредством реализа- ' ции динамических регистров (см. главу 9). Функции и константы языка С описываются с использованием n-,q нш - макросов 'Г4’ ‘Ь sqru: ___FLINT_API Спецификатор для С-функций 1 J и ' ___FLINT_API_A Спецификатор для ассемблерных функций v ___FLINT_API_DATA Спецификатор для констант как например, : - А ' ”'J extern int _FLINT_API add_l (CLINT, CLINT, CLINT); - < гн extern USHORT_____FLINT_API_DATA smallprimes[]; OJ или, соответственно, при использовании ассемблерных функций extern int _FLINT_API_A divj (CLINT, CLINT, CLINT, CLINT); 3 В современных компьютерах с виртуальной памятью, за исключением системы 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(dllimport). В заголовочном файле flint.h это уже учтено, и в этом случае для компиляции достаточно определить макрос FLINTJJSEDLL. Для дру- гих сред разработки следует представить аналогичные описания. Небольшой объём работы, связанной с инициализацией DLL, ис- пользующих FLINT/C, берет на себя функция FLINTInitJQ, которая задает начальные значения для генератора случайных чисел4 и генерирует набор динамических регистров (см. главу 9). Дополни- тельная функция FLINTExitJO освобождает динамические регистры. Вполне разумно, что инициализация производится не в каждом от- дельном процессе, использующем DLL, а выполняется один раз при старте DLL. Как правило, следует использовать функцию с опреде- ленной разработчиком сигнатурой и способом передачи парамет- ров, которая выполняется автоматически, когда исполняющая сис- тема {run-time) загружает DLL. Эта функция может взять на себя инициализацию FLINT/C и использовать две упомянутые выше функции. Все это следует учитывать при разработке DLL. Пришлось потрудиться, чтобы сделать программное обеспечение применимым для приложений, критичных к безопасности. Для этого в режиме безопасности локальные переменные функций, в частно- сти CLINT- и LINT-объекты, удаляются после использования по- F средством обнуления (записи на их место нулей). Для С-функций это достигается с помощью макроса PURGEVARS_L() и соответст- вующей функции purgevars_l(). Для С++-функций подобным же образом устроен деструктор ~LINT(). Ассемблерные функции зати- 4 Эти начальные значения получаются из 32-разрядных чисел, задаваемых системными часами. Для приложений, где безопасность критична, в качестве начальных значений советуем использо- вать подходящие случайные значения из достаточно большого интервала.
ГЛАВА 1. Введение 23 рают свою рабочую память. За удаление переменных, которые были переданы функциям в качестве аргументов, отвечают вызывающие функции. Если из-за определенных дополнительных затрат времени все же нужно пропустить удаление переменных, следует определить мак- рос FLINTJJNSECURE. Во время выполнения функция char* verstrJO выдает информацию о режимах, установленных во время компиляции. При этом дополнительно к метке версии Х.х в строке символов выводятся литеры «а» для ассемблерной поддержки и «s» для режима безопасности, если эти режимы были включены. 1.2. Законные условия использования программного обеспечения Данное программное обеспечение предназначено исключительно для личного использования. В этих целях программное обеспече- ние можно использовать, изменять и передавать при следующих условиях: 1. Запрещается изменять или удалять замечание об авторских правах. 2. Все изменения должны быть снабжены комментариями. Любое другое использование, в частности, использование программного обеспечения в коммерческих целях, требует наличия письменного разрешения от издателя или автора. Программное обеспечение было разработано и протестировано всеми доступными средствами. Так как никогда нельзя полностью исключить наличие ошибок, то ни автор, ни издатель не несут от- ветственности за прямой или косвенный ущерб, который может быть вызван применением или неприменением программного обес- печения, вне зависимости от того, с какими целями оно использо- валось. 1.3. Как связаться с автором Автор будет рад получить сведения об ошибках и любую другую полезную критику или замечания. Пожалуйста, пишите по адресу kryptographie@welschenbach.de5. 5 Обо всех ошибках в русском издании, пожалуйста, сообщайте редактору по адресу: Davel@semianov.com с пометкой «Криптография на С и C++» - Прим. ред.
ГЛАВА 2. Числовые форматы: представление больших чисел в языке С ХкИ;'. .-г'" (ГЦ; ,’ПЛ'ЬЬ ;п - vw 48 Итак, я придумал собственную систему записи больших чисел и, пользуясь случаем, разъясню ее в этой главе. Айзек Азимов, Дополнительное измерение Процесс, который привел эту форму на более высокую ступень организации, можно было изобразить и по-другому. Дж. Вебер, Форма, движение, цвет Приступая к созданию библиотеки функций для работы с большими числами, в первую очередь следует определить, как представлять и/ * эти числа в оперативной памяти компьютера. Эта задача требует г тщательно продуманного решения, поскольку впоследствии пере- смотреть его будет трудно. Конечно, всегда можно внести изме- нения во внутреннюю структуру библиотеки программ, но пользо- вательский интерфейс должен оставаться настолько стабильным, насколько это возможно в смысле «совместимости снизу вверх». Необходимо определить порядок размера чисел, которые придется обрабатывать, и тип данных, который будет использоваться для ко- дирования этих численных величин. В основе всех программ библиотеки 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 х 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-битовыми регистрами. Значение ULONGJVIAX определяет величину наибольших беззнаковых целых чисел, представимых скалярными типами (см. [Harb], стр. ИО)1. Наибольшая величина произведения двух чисел типа USHRT равна Oxffff * Oxffff = OxfffeOOOl и, следовательно, представима типом ULONG, где младшие 16 бит, в нашем примере 0x0001, можно выделить операцией приведения типов к типу USHRT. Реализация 1 Без учета используемых на практике нестандартных типов, таких как unsigned long long в GNU C/C++ и в некоторых других компиляторах С.
ГЛАВА 2. Числовые форматы 27 г/’ШШ ) .кОЛ » ЙОГА'. Ж : "Ч ТУГ' : основных арифметических функций пакета FLINT/C основывается на обсуждавшемся выше соотношении размеров между типами USHORT и ULONG. Аналогичным образом, используя типы данных длиной 32 бита и 64 бита вместо USHORT и ULONG, можно сократить время вычис- лений для операций умножения, деления и возведения в степень почти на 25 процентов. Такие возможности можно реализовать с помощью функций, написанных на Ассемблере, с прямым досту- пом к 64-битовым результатам машинных команд умножения и деления, или с помощью процессора с 64-битовыми регистрами, который также позволяет приложениям на языке С без потерь хранить полученные результаты в типе ULONG. Пакет FLINT/C содержит несколько примеров того, как можно ускорить получение результатов, используя арифметические ассемблерные функции (см. главу 18). Следующий вопрос - как упорядочить USHORT-разряды внутри вектора. Можно рассмотреть две возможности: слева направо, с убыванием значения разрядов от меньшего адреса ячейки памяти к большему, или наоборот, с возрастанием значения разрядов от меньшего адреса к большему. Второй вариант, противоположный нашей обычной системе обозначений, удобен тем, что размер чисел с постоянным адресом можно изменять просто добавляя разряды и не перемещая ничего в памяти. Таким образом, выбор ясен: разря- ды нашего числового представления возрастают с возрастанием адресов ячейки памяти или индексов вектора. В дальнейшем число разрядов будет рассматриваться как часть этого представления и храниться в первом элементе вектора. Таким образом, представление чисел большой длины в памяти выглядит так: п = (1пхп2... 0 < I < CLINTMAXDIGIT, 0 < < В (z = 1, /), где В обозначает основание числового представления; для пакета FLINT/C В := 216 = 65536. Это значение В будет постоянно участво- вать в дальнейшем изложении. Константа CLINTMAXDIGIT опреде- ляет максимальное число разрядов в CLINT-объекте. Нуль представляется длиной I = 0. Значение п числа, представляе- мого FLINT/C-переменной nJ, вычисляется как п_1[0] если nJ[0] > О, Z = 1 О, в противном случае. Если п > 0, то младший разряд числа п по основанию В задается nj[1], а старший разряд - nJ[nJ[0]]. Число разрядов nJ[0] в дальнейшем будет считываться макросом DIGITS_L (nJ) и поме- щаться в / макросом SETDIGITS_L (nJ, I). Соответственно макросы
28 Криптография на Си и C++ в действии LSDPTR_L(n_l) и MSDPTR_L(nJ) обеспечивают доступ к младшему и старшему разрядам п_1, причём каждый из них возвращает указа- тель на запрашиваемый разряд. Использование макросов, описан- ных в библиотеке flint.h, обеспечивает независимость от реального представления числа. Поскольку для натуральных чисел знак не нужен, у нас теперь есть все элементы, необходимые для представления этих чисел. Соот- ветствующий тип данных мы определяем следующим образом: typedef unsigned short clint; typedef clint CLINT[CLINTMAXDIGIT + 1]; В соответствии с этим, большое число описывается так: CLINT nJ; Описание параметров функции типа CLINT следует из записи CLINT 2 nJ в заголовке функции. Определение указателя myptrj на CLINT- объект осуществляется посредством CLINTPTR myptrj или clint *myptrj. В зависимости от установки константы 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], соответст- 2 В связи с этим сравните главы 4 и 9 чрезвычайно интересной книги [Lind], где приводится подроб- ное объяснение, в каких случаях в языке С векторы и указатели эквивалентны, а в каких - нет, и, что самое главное, к каким ошибкам может привести неправильное понимание этих случаев.
ГЛАВА 2. Числовые форматы 29 венно [О, 2МАХ2-1]. Обозначим величину 5МАХд-1 = 2МАХ2 ~ 1 через 7Vmax. Это наибольшее натуральное число, представимое С LI NT-объектом. Некоторым функциям приходится работать с числами, у которых разрядов больше, чем допускается CLINT-объектом. Для этих слу- чаев определяются следующие варианты CLINT-типа: typedef unsigned short CLINTD[1+(CLINTMAXDIGIT«1)]; и typedef unsigned short CLINTQ[1+(CLINTMAXDIGIT«2)]; которые могут содержать в два (соответственно в четыре) раза больше разрядов. В качестве вспомогательного средства при программировании модуль flint.с определяет константы nul_l, 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-объектов тоже оканчиваются на «_1». Далее для простоты, если позволяют условия, мы будем приравнивать CLINT-объект nJ тому значению, которое он представляет. Представление FLINT/C-функций начинается с заголовка, который содержит синтаксическое и семантическое описание интерфейса функции. Такие заголовки функций обычно выглядят следующим образом: Функция: Краткое описание функции Синтаксис: int fJ (CLINT aj, CLINT bj, CLINT cj); Вход: a J, bj (операнды) Выход: cj (результат) Возврат: 0, если все в порядке предупреждение или сообщение об ошибке в противном случае Здесь нужно различать между собой значения выхода и возврата. В то время как выход относится к значениям, которые функция хранит в переданных аргументах, под возвратом подразумеваются значения, которые функция возвращает посредством команды return. За исключением нескольких случаев (например, функции ldj(), п. 10.3, и twofactj(), п. 10.4.1), значение возврата содержит сведения о состоянии или сообщения об ошибке. Другие параметры, не связанные с выходом, функция не изменяет. Обращения вида f J(aJ, bj, aj), где aj и bj - аргументы и в конце
32 Криптография на Си и C++ в действии -Oh > * •; hlW’JtA ..Д': / •ol/J .’•« . ггнь 'hO Ж 'JO '» “-Г1 .* s 1*1 4 - > ЫГ, ; : ' U '/ I f / A, вычислений прежнее значение a_l затирается результатом, вполне допустимы, так как результат записывается в возвращаемую пере- менную только после завершения операции. Применяя термин про- граммирования на ассемблере, можно сказать, что в этом случае переменная а_1 используется как сумматор. Этот прием поддержи- вается всеми FLlNT/C-функциями. В СLI NT-объекте nJ имеются ведущие нули, если для некоторого значения I выполняется следующее условие (DIGITSJ. (nJ) == I) && (I > 0) && (nj[l] == 0); Ведущие нули являются избыточными, так как они увеличивают длину представления числа, не влияя при этом на его значение. Однако в системе обозначений числа ведущие нули допустимы, поэтому их не следует просто игнорировать. Конечно, при реали- зации ведущие нули являются обременительной деталью, но они способствуют вводу данных с внешних источников и таким обра- зом поддерживают стабильность всех функций. В пакете FLINT/C все функции понимают CLINT-числа с ведущими нулями, но не генерируют их. Следующее положение относится к поведению арифметических функций в случае переполнения, которое происходит, если резуль- тат арифметической операции слишком велик для заданного типа. Хотя в некоторых публикациях по С говорится, что поведение программы при переполнении зависит от реализации, стандарт языка С тем не менее точно регулирует случай переполнения при арифметических операциях с беззнаковыми целыми типами данных. Там утверждается, что следует применять арифметику по модулю 2”, когда тип данных представляет целые длиной п бит (см. [Harb], п. 5.1.2). Соответственно, в случае переполнения основные арифметические функции, описываемые ниже, выдают результат, приведенный по модулю (Nmax + 1), то есть остаток от целочисленного деления на 2Vmax + 1 (см. п. 4.3 и главу 5). В случае потери значимости (отрицательного переполнения), которая проис- ходит при получении отрицательного результата, на выход выда- ется положительный вычет по модулю (Nmax + !)• Таким образом, FLINT/C-функции согласуются с арифметикой в соответствии со стандартом языка С. Если обнаруживается переполнение или потеря значимости, ариф- метическая функция возвращает соответствующий код ошибки. Этот код и другие коды ошибок, приведенные в таблице 3.1, описаны в заголовочном файле flint.h.
ГЛАВА 3. Семантика интерфейса 33 Таблииа 3.7. Код ошибки Интерпретация Колы ошибок FLINT/C E_CLINT_BOR Недопустимое основание в str2clint_l() (см. главу 8) E_CLINT_DBZ Деление на нуль E_CLINT_MAL Ошибка при распределении ресурсов памяти E_CLINT_MOD Четные модули в умножении по Монтгомери E_CLINT_NIOR Регистр недоступен (см. главу 9) E_CLINT_NPT Пустой указатель в качестве аргумента E_CLINT_OFL Переполнение E_CLINT_UFL Потеря значимости
ГЛАВА 4. Основные операции Таким образом, вычисления можно считать ос- новой всех искусств. Адам Райс, Вычисления, 1574 А вы, бедные создания, совершенно бесполезны. Посмотрите на меня - я нужна всем. Эзоп, Ель и ежевика Для выполнения «математических» трюков в этой главе необходимо одно маленькое условие - вы должны знать таблицу умножения до 10... туда и обратно. Артур Бенджамин, Майкл Б. Шермер, Матемагия Любой вычислительный пакет программ составляется из функций, выполняющих основные арифметические операции: сложение, вы- читание, умножение и деление. Производительность всего пакета держится на двух последних операциях, поэтому нужно очень аккуратно подходить к выбору и реализации соответствующих алгоритмов. К счастью, во втором томе классического «Искусства программирования для ЭВМ» Дональда Кнута можно найти боль- шую часть из того, что требуется для этой части пакета FLINT/C. Прежде чем заниматься основными функциями, введем операцию cpyj(), копирующую один CLINT-объект в другой, и стр_1(), срав- нивающую размер двух CLINT-значений. Более строгое описание см. в п. 7.4 и в главе 8. Отметим, что в этой главе мы строим основные арифметические функции как единое целое, тогда как в главе 5 будет разумнее «посмотреть что у них внутри» и ввести ряд дополнительных операций: отбрасывание старших нулей, обработку переполнения и потери значащих разрядов, - сохраняя неизменными синтаксис и семантику этих функций. Но для понимания этой главы такие под- робности не нужны - что ж, забудем пока о них.
36 Криптография на Си и C++ в действии 4.1. Сложение и вычитание Понятие «дальнейшие вычисления» означает: «к целому числу П! прибавить целое число п2», ; . а результат этих дальнейших вычислений - целое число s - называется «результатом сло- жения» или «суммой ni и п2» и записывается П1 + п2. Леопольд Кронекер, Об идее чисел Поскольку операции сложения и вычитания отличаются лишь зна- ком, соответствующие алгоритмы практически одинаковы и могут быть рассмотрены одновременно. Пусть операнды а и b заданы в системе счисления с основанием В: т-1 а~(ат_}ат_2...а0)в=^а:В‘ ’ 0<а,<В, i=0 0 </>,< В, /=0 и пусть для определенности а>Ь. Для операции сложения это условие ни на что не влияет, поскольку слагаемые всегда можно поменять местами. Для операции вычитания оно означает, что раз- ность является неотрицательным числом и, следовательно, может быть представлена CLINT-объектом без приведения по модулю Mnax + 1- Вот основные шаги сложения. Алгоритм вычисления суммы а + Ъ 1. Положить i <— 0 и с <— 0. 2. Вычислить t <— сц + bi + с, <— t mod В и с <— [_t/BJ. 3. Положить i <— i + 1. При I < п - 1 вернуться на шаг 2. . 4. Вычислить t <— at + с, si <— t mod В и с <г- [t/BJ. 5. Положить i <— i + 1. При i < т - 1 вернуться на шаг 4. 6. Положить sm <— с. 7. Результат: 5 = (smsm_x.. .s^B. На шаге 2 разряды слагаемых суммируются с переносом, при этом младшая часть результата записывается в разряд суммы, а старшая переносится в следующий разряд. Как только мы дойдем до старше- го разряда одного из слагаемых, на шаге 4 все оставшиеся разряды
г ГЛАВА 4. Основные операции 37 - э? второго слагаемого складываются со всеми оставшимися перено- сами. До тех пор пока не обработан последний разряд слагаемого, младшая часть записывается в разряд суммы, а старшая переносится в следующий разряд. И, наконец, самый последний перенос (если он есть) записывается в самый старший разряд суммы. В случае вычитания, умножения и деления шаги 2 и 4 имеют тот же вид. Соответствующий код типичен для арифметических функций: 1 s = (USHORT)(carry = (ULONG)a + (ULONG)b + (ULONG)(USHORT)(carry » BITPERDGT)); им*-1’ Промежуточное значение г, участвующее в алгоритме, представле- но переменной carry типа ULONG, в которую записывается сумма разрядов ah и переноса с предыдущей операции. Полученный новый разряд si записывается в младшую часть переменной carry и приведением типов преобразуется в значение типа USHORT. Возникающий перенос записывается в старшую часть переменной carry до следующего шага. При реализации этого алгоритма с помощью функции add_l() может возникнуть переполнение; в этом случае нужно привести сумму по модулю Nmax+ 1. Функция: Сложение Синтаксис: int addj (CLINT aj, CLINT bj, CLINT sj); Вход: aj, bj (слагаемые) Выход: sj (сумма) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int addj (CLINT aj, CLINT bj, CLINT sj) { clint ss_l[CLINTMAXSHORT + 1]; clint *msdptra_l, *msdptrb_l; clint *aptr_l, *bptr_l, *sptrj = LSDPTR_L (ss_l); ULONG carry = OL; int OFL = 0; Автор этого компактного выражения - мой коллега Robert Hammelrath.
38 Криптография на Си и C++ в действии В цикле сложения используются указатели. Проверяется, у какого из слагаемых число разрядов больше. Инициируются указатели aptrj и msdaptrj, соответствующие самому младшему и самому старшему разряду большего слагаемого (или а_1, если слагаемые имеют равную длину). Аналогично, указатели bptrj и msdbptrj соответствуют самому младшему и самому старшему разряду меньшего слагаемого, или bj. Инициализация выполняется с помошью макросов LSDPTR_L() для младших разрядов и MSDPTR_L() для старших разрядов CLINT-объекта. Макрос DIGITS_L (а_1) определяет число разрядов CLINT-объекта а_1, а макрос SETDIGITS_L (а_1, п) устанавливает число разрядов сла- гаемого а_1 равным п. if (DIGITS J_ (а_1) < DIGITS J. (bj)) { aptrj = LSDPTFLL (bj); bptrj = LSDPTFLL (aj); msdptraj = MSDPTFLL (bj); msdptrbj = MSDPTFLL (aj); SETDIGITS J_ (ssj, DIGITS J. (bj)); else { aptrj = LSDPTFLL (aj); bptrj = LSDPTRJ- (bj); msdptraj = MSDPTFLL (aj); msdptrbj = MSDPTFLL (bj); SETDIGITS_L (ssj, DIGITS_L (aj)); На первом цикле процедуры addj разряды переменных aj и bj суммируются и записываются в результирующую переменную ssj. Появление ведущих нулей не вызывает никаких проблем - они участвуют в вычислениях и отбрасываются при копировании результата в sj. Движение осуществляется от младших разрядов слагаемого b J к старшим разрядам. Это в точности соответствует школьному сложению в «столбик». Как и было обешано, исполь- зуем перенос. while (bptrj <= msdptrbj) {
ГЛАВА 4. Основные операции 39 *sptrj++ = (USHORT)(carry = (ULONG)*aptrJ++ + (ULONG)*bptrJ++ + (ULONG)(USHORT)(carry » BITPERDGT)); } Преобразуем USHORT-значения *aptr и *bptr путем приведения типов в ULONG и сложим. К полученной сумме прибавим пере- нос из предыдущей итерации. Результатом является значение ти- па ULONG, содержащее перенос в старшем слове. Это значение ч присваивается переменной carry и хранится в ней до следующей ( ) итерации. Из суммы берем младшее слово - это и есть результи- । руюший разряд типа USHORT. Перенос, хранящийся в старшем ® слове carry, после сдвига на BITPERDGT и преобразования типов участвует в следующей итерации. На втором цикле оставшиеся разряды переменной а_1 складыва- ются с переносом (если он есть); результат записывается в s_l. while (aptrj <= msdptraj) { *sptrj++ = (USHORT)(carry = (ULONG)*aptrJ++ + (ULONG)(USHORT)(carry » BITPERDGT)); } Если на втором цикле возникает перенос, то результат будет на один разряд длиннее, чем слагаемое а_1. Если результат превышает I максимальное значение Nmax, допускаемое типом CLINT, то его z нужно привести по модулю (Nmax + 1) (см. главу 5), как это дела- ется для стандартных беззнаковых типов. В этом случае возвра- щается уведомление об ошибке E_CLINT_OFL. if (carry & BASE) { *sptr_l = 1; SETDIGITS J_ (ssj, DIGITS_L (ssj) + 1); if (DIGITS_L (ssj) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? 7 ANDMAX_L (ssj); OFL = E_CLINT_OFL; /* Привести по модулю (Nmax +1)7
40 Криптография на Си и C++ в действии } cpy_l (s_l, ss_l); return OFL; } I' I и Временная сложность t всех представленных здесь процедур сло- жения и вычитания равна t = 0(h), то есть пропорциональна числу разрядов большего операнда. Что ж, сложение мы рассмотрели, теперь перейдем к вычислению ‘1 разности двух чисел а и Ь, заданных в системе счисления с основа- нием В: е а = (^/n-i^/n-2- • -ао)в ^b- (bn..ibn_2- • -Ьо)в Алгоритм вычисления разности а -Ь 1. Положить i <— 0 и с <— 1. 2. При с = 1 вычислить t <— В + иначе t <— В - 1 + at? - bj. 3. Положить Si <— t mod В и с <— \_t/BJ. 4. Положить i <— i + 1. При i < н - 1 вернуться на шаг 2. г 5. При с = 1 вычислить t <— В + а,; иначе t <- В - 1 + 6. Вычислить Si <— t mod В и с <— [_t!BJ. ' О'- г . • 7. Положить i <— i + 1. При i < т - 1 вернуться на шаг 5. 8. Результат: 5 = (sm_xsm_2-• sq)b. W* *С *,М' '4J Ч Вычитание реализуется так же, как и сложение, за исключением следующего: ✓ С помощью переменной carry типа ULONG мы «занимаем» из предыдущего старшего разряда уменьшаемого в том случае, если текущий разряд уменьшаемого меньше соответствующего разряда вычитаемого. V Здесь нужно следить уже не за переполнением, а за возможной потерей значащих разрядов; в этом случае результат вычитания вообще-то будет отрицательным; однако, поскольку тип CLINT ( беззнаковый, нужно выполнить приведение по модулю (Nmax+ 1) (см. главу 5). Эта ситуация выявляется, когда функция возвращает код ошибки E_CLINT_UFL. ✓ И последнее: все ведущие нулевые разряды отбрасываются. Таким образом, получается такая функция вычисления разности чисел а_1 и Ь_1 типа CLINT.
ГЛАВА 4. Основные операции 41 функция: Вычитание Синтаксис: int subj (CLINT aaj, CLINT bbj, CLINT dj); Вход: aaj (уменьшаемое), bbj (вычитаемое) Выход: dj (разность) Возврат: E_CLINT_OK, если все в порядке E_CLINT_UFL в случае потери значащих разрядов int subj (CLINT aaj, CLINT bbj, CLINT dj) { CLINT bj; clint a_l[CLINTMAXSHORT + 1]; /* Добавляем в a_l еще один разряд */ clint *msdptra_l, *msdptrb_J; clint *aptrj = LSDPTFLL (aJ); clint *bptrj = LSDPTFLL (bj); clint *dptrj = LSDPTFLL (dj); ULONG carry = OL; intUFL = O; cpyj (aj, aaj); : cpyj (bj, bbj); msdptraj = MSDPTFLL (aj); msdptrbj = MSDPTFLL (bj); Если aj < bj, то будем вычитать bj не из aj, а из максимально возможного значения Nmax. К полученной разности прибавим (уменьшаемое + 1), то есть все вычисления выполняем по модулю (Nmax +1 )• Значение Nmax генерируем с помошью функции setmaxJO. if (LT_L (aj, bj)) { setmax J (aj); msdptraj = aj + CLINTMAXDIGIT;
42 Криптография на Си и C++ в действии SETDIGITS-L (d_l, CLINTMAXDIGIT); UFL = E_CLINT_UFL; • } else { SETDIGITSJ. (dj, DIGITS_L (aj)); C: . В } while (bptrj <= msdptrbj) { *dptr_l++ = (USHORT)(carry = (ULONG)*aptr_l++ - (ULONG)*bptrJ++ - ((carry & BASE) » BITPERDGT)); } while (aptrj <= msdptraj) *dptr_l++ = (USHORT)(carry = (ULONG)*aptr_l++ - ((carry & BASE) » BITPERDGT)); ) RMLDZRS_L (dj); Складываем разность Nmax - bj, записанную в d_l, с величиной (уменьшаемое + 1). Результат: dj. '?м- * > '* л i if (UFL) ь 'ф I { addj (d_l, aa_l, d_l); inc_l (dj); ' 4‘‘ .AW'»/ (« } return UFL; }
ГДАВА 4. Основные операции 43 Помимо рассмотренных функций add_l() и sub_l() введем еще две специальные функции, второй аргумент которых имеет тип USHORT, а не CLINT. Такие функции будем называть смешанными и обозначать префиксом «и», например uaddj() и usubj(). Преобра- зование значения типа USHORT в CLINT-объект осуществляется с помощью функции u2clint_l(), с которой мы познакомимся в главе 8. функция: Синтаксис: Вход: Выход: Возврат: Смешанное сложение переменных типа CLINT и USHORT int uaddj (CLINT aj, USHORT b, CLINT sj); aj, b (слагаемые) sj (сумма) E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int uaddj (CLINT aj, USHORT b, CLINT sj) { int err; CLINT tmpj; u2clintj (tmpj, b); err = addj (aj, tmpj, sj); return err; } Функция: Вычитание числа типа USHORT из числа типа CLINT Синтаксис: int usubj (CLINT aj, USHORT b, CLINT dj); Вход: a J (уменьшаемое), 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 (aj, tmpj, dj); return err; } Еще два важных частных случая сложения и вычитания: увеличе- ние и уменьшение CLINT-объекта на 1 (инкремент и декремент). Реализуем их в виде функций incj() и decj() в виде процедуры- сумматора: новое значение операнда будем записывать на место старого - очень практичный способ, используемый во многих алго- ритмах. Неудивительно, что реализация функций incj() и decj() осуществ- ляется по аналогии с функциями addj() и subj(). Они точно так же контролируют переполнение и потерю значащих разрядов, возвра- щая соответствующие коды ошибки E_CLINT_OFL и E_CLINT_UFL. Функция: Увеличение CLINT-объекта на 1 Синтаксис: int incj (CLINT aj); Вход: aj (слагаемое) Выход: aj (сумма) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int incj (CLINT a J) { clint ‘msdptraj, ‘aptrj = LSDPTR_L (aj); ULONG carry = BASE; int OFL = 0; msdptraj = MSDPTFLL (aj); • * ,, while ((aptrj <= msdptraj) && (carry & BASE)) { ‘aptrj = (USHORT)(carry = 1UL + (ULONG)‘aptrJ); aptrj++; } if ((aptrj > msdptraj) && (carry & BASE))
. Основные операции 45 { *aptrJ = 1; SETDIGITS J_ (aj, DIGITS J_ (aj) + 1); if (DIGITS J_ (aj) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? 7 { SETZERO_L (aJ); /* Привести по модулю (Nmax +1)7 OFL = E_CLINT_OFL; } } return OFL; ) Функция: Уменьшение CLINT-объекта на 1 Синтаксис: int decj (CLINT aj); Вход: aj (уменьшаемое) Выход: aj (разность) Возврат: EJDLINT_OK, если все в порядке E_CLINT_UFL в случае потери значащих разрядов int d tv:. decj (CLINT aj) clint *msdptraj, *aptr_l = LSDPTR_L (aj); ULONG carry = DBASEMINONE; if (EQZ_L (aj)) /* Потеря значащих разрядов ? 7 /* Привести по модулю maxj 7 setmax J (aj); return E_CLINT_UFL; msdptraj = MSDPTRJ. (aj); while ((aptrj <= msdptraj) &&
Криптография на Си и C++ в действии (carry & (BASEMINONEL « BITPERDGT))) { *aptrj = (USHORT)(carry = (ULONG)*aptrJ - 1L); aptr_l++; } RMLDZRS_L (aJ); return E_CLINT_OK; 2. Умножение Если все слагаемые nb n2, n3, ..., nr равны од- ному и тому же целому числу п, то сложение называется «умножением числа г на целое чис- ло п» и обозначается гц + п2 + п3 + ... + пг = гп. Леопольд Кронекер, Об идее чисел Функция умножения играет основную роль в пакете FLINT/C, яв- ляется одной из наиболее трудоемких и вместе с функцией деления определяет время работы многих алгоритмов. В отличие от сложе- ния и вычитания, с которыми мы уже знакомы, сложность класси- .. . „ ческих алгоритмов умножения и деления - квадратичная от числа разрядов аргументов, недаром Дональд Кнут назвал одну из глав «Искусства программирования для ЭВМ» «Как быстро мы можем умножать?». На сегодняшний день в литературе опубликовано множество мето- дов умножения больших и очень больших чисел, среди которых есть и весьма трудные. Одним из самых трудных является алго- ритм, предложенный А. Шёнхаге и В. Штрассеном, в основе кото- рого лежит быстрое преобразование Фурье над конечными полями. Время работы этого алгоритма оценивается величиной О(п log п log log и), где и - число разрядов аргументов (см. [Knut], п. 4.3.3). Недостаток этого и подобных алгоритмов заключается в том, что выигрыш по скорости по сравнению с классическими методами сложности О(п2) достигается лишь когда длина сомножи- телей составляет порядка 8000-10000 двоичных разрядов. До ис- пользования таких чисел к криптографии пока еще очень далеко. Сначала реализуем в пакете FLINT/C основной «школьный» метод умножения, основанный на «Алгоритме М» Кнута (см. [Knut], п. 4.3.1), и попробуем «выжать» из него максимальную скорость. Затем плотно займемся возведением в квадрат - многообещающей
рЛАВА 4. Основные операции 47 операцией в смысле снижения вычислительных затрат - и для обоих случаев попробуем применить алгоритм Карацубы, асимптотически более быстрый, чем <Э(п2).2 Алгоритм Карацубы весьма любопытен и привлекателен своей кажущейся простотой, так что желающие могут заняться его реализацией как-нибудь (дождливым) воскрес- ным днем. А пока мы посмотрим, дает ли что-либо этот алгоритм для библиотеки FLINT/C. 4.2.1. Школьный метод Рассмотрим умножение чисел а и Ь, заданных в системе счисления с основанием В: т-1 а:=(а„_1ат.2...а0)в = У^а,В', 0<а; <В, i=0 b-.= (bn_xbn_2...b0)B=!yjbiBi, Q<bt<B. i=0 Как нас учили в школе, произведение ab можно вычислить по схеме рис. 4.1 (для т = п = 3). Рисунок 4.1. Вычисления при умножении (Д2Д1^о)д ‘ (Ь2Ь\Ьо)В с20 Р20 Р10 Р00 + С21 Р21 Рн Poi + с22 Р22 Р12 Р02 <Р5 Р4 Рз Р2 Р1 Ро)в Сначала вычисляем частичные произведения (д2Я1Яо)я ’ для j = 0’ 1, 2: значения - это младшие разряды значения (яД + перенос), где apj - внутреннее произведение, а с2; - старшие разряды значе- ния p2j. Затем суммируем частичные произведения и получаем про- изведение р = (p5P4P3P2PlPo)fi- В общем случае произведение p = ab равно п-1 т-1 р = J=0 /=0 Произведение двух операндов длины т и п имеет длину не менее т + п - 1 и не более т + п разрядов. Число элементарных умноже- ний (в которых операнды меньше основания В) равно нт. 2 Когда про алгоритм говорят, что он асимптотически более быстрый, это означает, что чем боль- ше входные значения алгоритма, тем больше заметно увеличение скорости. Однако не следует преждевременно впадать в эйфорию - для наших целей это улучшение может вообще не играть никакой роли.
48 Криптография на Си и C++ в действии Функцию умножения можно реализовать по указанной схеме, то есть сначала вычислим и запомним все частичные произведения, а затем просуммируем их, домножая на соответствующую степень основания В. Этот школьный метод вполне годится для вычисления с карандашом и бумагой, но для компьютерной реализации он слишком громоздкий. Более эффективная альтернатива - сразу при- бавлять внутренние произведения atbj и переносы с с предыдущих шагов к результирующему разряду pi+j. Итоговое значение для каж- дой пары (/, J) записываем в переменную t: t <— pi+j + ciibj + с, где t представимо в виде t = kB + /, 0 <к, 1 < В. V Тогда pi+j + aibj + с<В-1 + (В- 1)(В - 1) + В - 1 = =(В - 1)В + В- 1 = В2 — 1 <в2. ж.* он . Текущие значения разрядов результата определяются из представ- ления переменной t присваиванием pi+j <— 1. Выполняем новый перенос: с «— к. Таким образом, теперь наш алгоритм умножения включает только внешний цикл, вычисляющий частичные произведения а^Ь^Ь^... Ьо)в> и внутренний цикл, вычисляющий внутренние произведения a^bj, где j = 0, ..., п - 1, и значения t и р/±/. Вот этот алгоритм. Алгоритм умножения 1. Для i = 0, ..., п - 1 положить pt <— 0. 2. Положить 1 <— 0. 3. Положить j <— 0 и с <— 0. 4. Положить t <— pi+j + aibj + с, pi+7 <-1 mod В и с <- \_t/BJ. 5. Положить j <r-j + 1. При j < n - 1 вернуться на шаг 4. 6. Положить pi+n «— с. 7. Положить i <— i + 1. При i < т - 1 вернуться на шаг 3. 8. Результат: р = (рт+п_\рт+п_2.• -Ро)/?- Этот основной цикл является ядром следующей реализации алгорит- ма умножения. В соответствии с полученными оценками, на шаге 4 для переменной t требуется точное представление чисел, меньших В1. Так же как и в случае сложения, представим внутренние произ- ведения t типом ULONG. Заметим, что переменная t используется
ГЛАВА 4. Основные операции 49 неявно, а разряды произведения pi+j и переносы с выделяются внут- ри одного и того же выражения так же, как это делалось в функции сложения (см. стр. 37). Начальные значения будем задавать более эффективной процедурой, чем на шаге 1 алгоритма. функция: Умножение Синтаксис: int mulj (CLINT f1 J, CLINT f2J, CLINT ppj); Вход: f1 J, f2J (сомножители) Выход: ppj (произведение) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int mulj (CLINT И J, CLINT f2J, CLINT ppj) { register clint *pptr_l, *bptr_l; CLINT aaj, bbj; CLINTD pj; clint *aj, *b_l, *aptrj, *csptr_l, *msdptra_l, *msdptrbj; USHORT av; ULONG carry; int OFL = 0; Сначала опишем переменные: результат будем записывать в pj, то есть эта переменная должна быть двойной длины. Сначала рассмотрим случай, когда один из сомножителей (а значит и про- изведение) равен нулю. Иначе заносим сомножители в аа_1 и bbj и убираем ведущие нули. + 1 if (EQZ_L (f1 J) || EQZ_L (f2_l)) { SETZERO_L (ppj); return E_CLINT_OK; } cpyj (aaj, f1 J); cpyj (bbj, f2J);
50 Криптография на Си и C++ в действии В соответствии с описанием задаем указателям а_1 и Ь_1 адреса аа_1 и bb_l. Если число разрядов в аа_1 меньше, чем в bb_l, вы- полняем логическую перестановку: указатель а_1 всегда соответ- ствует операнду с большим числом разрядов. if (DIGITS J_ (aaj) < DIG ITS J_ (bbj)) { a J = bbj; bj = aaj; } else { a J = aaj; bj = bbj; } msdptraj = a J + *aj; msdptrbj = bj + *bj; Для экономии времени не выполняем инициализацию, указанную выше, а вычисляем циклически частичное произведение Ь0)в • а0 и заносим результат в pn, pn_lz ..., рп. л’ carry = 0; av = *LSDPTRJ_ (aJ); for (bptrj = LSDPTRJ. (bj), pptrj = LSDPTRJ. (pj); bptrj <= msdptrbj; bptrj++, pptrj++) *pptr_l = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l + (ULONG)(USHORT)(carry » BITPERDGT)); } *pptr_l = (USHORT)(carry » BITPERDGT); Дальше идет вложенный иикл умножения, начиная с разряда а_1[2] переменной а_1.
ГЛАВА 4. Основные операции 51 for (csptrj = LSDPTR_L (рJ) + 1, aptrj = LSDPTR J. (aj) + 1; aptrj <= msdptraj; csptrj++, aptrj++) ' { carry = 0; un ut: av = *aptrj; 'V' for (bptrj = LSDPTRJ- (bj), pptrj = csptrj; bptrj <= msdptrbj; > bptrj++, pptrj++) { t 4 *PPtrJ = (USHORT)(carry = (ULONG)av * (ULONG)*bptrJ + (ULONG)*pptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); *pptrj = (USHORT)(carry » BITPERDGT); } Максимально возможная длина результата равна сумме длин а_1 и bj. Случай, когда длина результата оказывается на единицу мень- ше, выявляется макросом RMLDZRSJ-. SETDIGITS J_ (pj, DIGITS J. (aj) + DIGITS J. (bj)); RMLDZRSJ. (pj); Если результат превышает допустимые размеры для объектов типа CLINT, то он приводится по модулю (Nmax + 1), а флагу ошибки OFL присваивается значение E_CLINT_OFL. Приведенный по мо- дулю результат записывается в ppj. if (DIGITSJ_ (pj) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? 7 { ANDMAX_L (pj); /* Привести по модулю (Nmax +1)7 OFL = E_CLINT_OFL;
г 52 Криптография на Си и C++ в действии cpyj (pp_l, рJ); return OFL; } Время t = O(mri) выполнения умножения пропорционально произ- ведению длин т и п операндов. Для умножения, как и для сложе- ния, можно реализовать смешанную функцию, первый аргумент которой имеет тип CLINT, а второй - тип USHORT. Укороченная версия CLINT-умножения требует О(п) процессорных умножений. Однако этим результатом мы обязаны не какому-то усовершенст- вованию алгоритма, а просто малой длине USHORT-аргумента. Мы еще вернемся к этой функции, когда будем возводить в степень число типа USHORT (см. главу 6, функцию wmexpJO). Для реализации функции umul_() воспользуемся слегка модифици- рованной функцией mulj(). Функция: Умножение CLINT-объекта на число типа USHORT Синтаксис: int umulj (CLINT aaj, USHORT b, CLINT ppj); Вход: aaj, b (сомножители) Выход: ppj (произведение) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int umulj (CLINT aaj, USHORT b, CLINT ppj) { register clint *aptrj, *pptrj; CLINT aj; CLINTD pj; clint *msdptraj; ULONG carry; int OFL = 0; cpyj (aj, aaj); if (EQZ_L (aJ) || 0 == b) {
ГЛАВА 4* Основные операции SETZERO J_ (ppj); 53 return E_CLINT_OK; } Предварительная подготовка завершена, теперь в цикле выпол- няем умножение CLINT-аргумента на USHORT-аргумент, перенос записываем в старший USHORT-разряд CLINT-аргумента. msdptraj = MSDPTR_L (aj); carry = 0; for (aptrj = LSDPTR_L (aJ), pptrj = LSDPTRJ (pj); aptrj <= msdptraj; aptrj++, pptrj++) { ><.; *pptr_l = (USHORT)(carry = (ULONG)b * (ULONG)*aptr_l + (ULONG)(USHORT)(carry » BITPERDGT)); } Km ' ' *pptr_l = (USHORT)(carry » BITPERDGT); SETDIGITS J_ (p_l, DIGITSJ. (a_l) + 1); RMLDZRS_L (p_l); '►!№ if (DIGITSJ- (pj) > (USHORT)CLINTMAXDIGIT) ns Oiiv /* Переполнение ? 7 { ANDMAXJ. (pj); /* Привести по модулю (Nmax + 1)7 OFL = E_CLINT_OFL; } cpyJ (ppJ, pJ); return OFL; )
54 Криптография на Си и C++ в действии 4.2.2. А возведение в квадрат - быстрее | Возведение большого числа в квадрат требует значительно меньше! го числа умножений, чем умножение двух больших чисел, благодаря симметрии операндов. Это наблюдение становится еще более важ- ? ' * * ным, когда дело доходит до возведения в степень, и нам требуется ? ъ ' не одно, а сотни возведений в квадрат - тогда можно получить значи- j а '' тельное увеличение скорости. Снова обратимся к хорошо известной схеме умножения, на этот раз с двумя одинаковыми сомножителя- ми (я2<21Яо)д (см. рис. 4.2). («2«1^о)д ‘ Рисунок 4.2. Вычисления при возведении в квадрат + Я2а0 (цац a2ai «1«1 аоа{ + ^1^2 «0^2 (Р5 Р4 Рз Р2 Pi Ро)в Заметим, что внутренние произведения ащ вычисляются, во- первых, однократно для i = j (выделены полужирным шрифтом на рис. 4.2), и, во-вторых, дважды для i Ф j (на рисунке они обведены прямоугольниками). Таким образом, вместо девяти умножений можно выполнять всего три, удвоив слагаемые ащВ1^ при i < j. Тогда сумму внутренних произведений при возведении в квадрат можно переписать как л-1 л-2 л-1 л-1 р = = 2£ YaiajBi+J i,j=Q i=0 j=i+l j=Q Таким образом, число элементарных умножений по сравнению со «школьным» методом сокращается с п2 до и(и + 1 )/2. Вычисление последнего выражения для р естественно алгоритми- чески реализовать в виде двух вложенных циклов. V Ц 4 ’лё Алгоритм 1 возведения в квадрат 1. Для i = 0, ..., п - 1 положить<— 0. 2. Положить i <- 0. ! 3. Положить t <— p2l + аД P2i <— t mod В и с <- \j/BJ. 4. Положить j «— i + 1. При j = n -1 перейти на шаг 7. 5. Положить t <— Pi+j + 2a щ + с, pi+J- <— t mod В и с <— 6. Положить j <— j' + 1. При j < n - 1 вернуться на шаг 5. 7. Положить pi+n <— с.
ГЛАВА 4. Основные операции 55 8. Положить i <— i + 1. При i < п - 1 вернуться на шаг 3. 9. Резул ьтат: р = (p2n-iP2n-2 -Ро)в- Выбирая типы данных для представления переменных, следует учесть, что t может принимать значение (В - 1) + 2(5 - 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. Вычисление произведения разрядов с неравными индексами: положить i <г- 0. 3. Положить j <— / + 1 и с <— 0. 4. Положить t <— pi+J + a^j + с, pi+j <— t mod В и с <— \j/BJ. 5. Положить j <r-j + 1. При j < n - 1 вернуться на шаг 4. 6. Положить pi+n <— с. 7. Положить i <г- i + 1. При i < п - 2 вернуться на шаг 3. 8. Удвоение внутренних произведений: положить i <— 1 и с <— 0. 9. Положить t <— 2pi + с, pi <— t mod В и с <— [_t!BJ. 10. Положить i <— i + 1. При i < 2n - 2 вернуться на шаг 9. 11. Положить ргп-\ <— с. 12. Суммирование внутренних квадратов: положить i <- 0 и с <— 0. 13. Положить t <— p2t + а,2 + с, ри <— t mod В и с <— L^/Bj. 14. Положить t <— p2i+\ + с, Р2/+1 <— t mod В и с <— l_t/BJ. 15. Положить i <— i + 1. При i < п - 1 вернуться на шаг 13. 16. Положить р2п_х <- р2и-1 + с. Результат: р = (р2п-\Р2п-2-• -РоЪ-
56 Криптография на Си и C++ в действии При реализации на С вместо шага 1, по аналогии с умножением, вычисляем и запоминаем первое частичное произведение 6z0(tf„-i «л_2.. .а\)в. Функция: Синтаксис: Вход: Выход: Возврат: Возведение в квадрат nt sqr_l (CLINT fJ, CLINT ppj); LI (операнд) ppj (квадрат) E-CLINTJDK, если все в порядке E_CLINT_OFL в случае переполнения int sqrj (CLINT fJ, CLINT ppj) i А/- — register clint *pptrj, *bptrj; CLINT aj; CLINTD pj; clint *aptrj, *csptrj, *msdptraj, *msdptrbj, *msdptrcj; USHORT av; ULONG carry; int OFL = 0; п". s cpyj (aj, fJ); if (EQZ_L (aj)) Q —а э w I ..." • j ТШП i'J - (} , t SETZERO_L (ppj); return E_CLINT_OK; } msdptrbj = MSDPTR_L (aj); msdptraj = msdptrbj - 1; ( Инициализация результирующего вектора по указателю pptrj вы- полняется путем вычисления частичного произведения a0(an_ian_2--- aJB, по аналогии с умножением. Разряду р0 здесь не присваива- ется никакое значение; он должен быть нулевым. LSDPTRJ. (р_1) = 0;
ГЛАВА 4. Основные операции 57 carry = 0; av = *LSDPTRJ_ (aj); for (bptrj = LSDPTFLL (a_l) + 1, pptrj = LSDPTFLL (pj) + 1; bptrj <= msdptrbj; bptrj++, pptrj++) { *pptrj = (USHORT)(carry = (ULONG)av * (ULONG)*bptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); } *pptrj = (USHORT)(carry » BITPERDGT); Цикл суммирования внутренних произведений а,а;. for (aptrj = LSDPTRJ_ (aj) + 1, csptrj = LSDPTR_L (pj) + 3; aptrj <= msdptraj; aptrj++, csptrj += 2) { carry = 0; av = *aptrj; for (bptrj = aptrj + 1, pptrj = csptrj; bptrj <= msdptrbj; bptrj++, pptrJ++) { ‘pptrj = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l + (ULONG)‘pptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); } ‘pptrj = (USHORT)(carry » BITPERDGT); } msdptrcj = pptrj; Умножение промежуточного результата из pptrj на 2 выполняем с помошью сдвига (см. также п. 7.1). carry = 0; for (pptrj = LSDPTRJ. (pj); pptr_l <= msdptrcj; pptrj++) {
58 Криптография на Си и C++ в действии *pptrj = (USHORT)(carry = (((ULONG)*pptr_l) « 1) + (ULONG)(USHORT)(carry » BITPERDGT)); } *pptr_l = (USHORT)(carry » BITPERDGT); / Теперь вычисляем «главную диагональ». carry = 0; for (bptrj = LSDPTR_L (aj), pptrj = LSDPTR_L (pj); bptrj <= msdptrbj; bptrj++, pptrj++) { *pptrj = (USHORT)(carry = (ULONG)*bptrJ * (ULONG)*bptrJ + (ULONG)*pptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); pptrj++; *pptrj = (USHORT)(carry = (ULONGfpptrJ + (carry » BITPERDGT)); } Все остальное - так же, как в умножении. SETDIGITS J_ (р J, DIGITS J. (aj) « 1); RMLDZRS.L (pj); if (DIGITSJ_ (pj) > (USHORT)CLINTMAXDIGIT) /* Переполнение? 7 { ANDMAX-L. (pj); /* Привести по модулю (Nmax +1)7 OFL = E_CLINT_OFL; } cpyj (ppj, pj); return OFL;
ГЛАВА 4. Основные операции 59 Время работы процедуры возведения в квадрат равно О(м2), то есть квадратичное от длины операнда. Однако, поскольку здесь требуется п(п + 1)/2 элементарных умножений, эта функция почти в два раза быстрее, чем умножение. 4.2.3. Поможет ли метод Карацубы? /к ,vi Дух умножения и деления разрушил все вокруг и устремился к отдельной части целого. Стэн Надольный, Бог дерзости Как мы и обещали, рассмотрим метод умножения, носящий имя русского математика А. Карацубы, опубликовавшего несколько вариантов этого метода (см. [Knut], п. 4.3.3). Пусть числа а и Ь - натуральные длины п = 2к разрядов в системе счисления с основа- нием В. Представим а и b в системе счисления с основанием Вк: а = b = Если умножать а на b традиционным спосо- бом, то для вычисления произведения ab = В kci\b\ + В (aob\ + ci\Ьо) + требуется четыре умножения по основанию Вк и, следовательно, п2 = 4к2 элементарных умножений по основанию В. Однако если положить Со := aobo, := €ZiZ?i, <?2 := (aQ + tfi)(Z>o + bf) -cQ- сь / с то ab = Вк(Вкс\ + с%) + Со- Оказывается, теперь для вычисления произведения ab нужно всего три умножения чисел по основанию Вк или, что то же самое, 3£2 умножений по основанию В плюс несколько операций сложения и сдвига (умножение на Вк можно реализовать сдвигом на к разрядов в системе счисления с основанием В; см. п. 7.1). Предположим, что число п разрядов сомножителей а и b является степенью числа 2. Тогда, рекурсивно применяя указанную процедуру для вычисления частичных произведений, можно свести алгоритм к выполнению только элементарных умножений по основанию В. Откуда получаем 31оё2Л -/г10^3 =/г1>585 элементарных умножений вместо п2 (в класси- ческом методе); дополнительное время потребуется еще для опера- ций сложения и сдвига. Применительно к возведению в квадрат этот процесс несколько уп- рощается. Если Со := «о .
60 Криптография на Си и C++ в действии Cl := a2 , г'Д . I-.'. . ci(«о + fli)2 - cq- Ci, TO Cl2 = B^^B^Ci + C2) + Cq. ’ /pUH J--? f • На нас работает еще и то, что при возведении в квадрат оба сомно- жителя всегда имеют одну и ту же длину, что далеко не всегда выполняется в случае умножения. Следует, однако, помнить, что рекурсия тоже требует определенных затрат времени, так что наде- яться на какие-либо преимущества перед классическим методом, не обремененным рекурсией, можно лишь при работе с большими числами. . (НИ Чтобы иметь полную информацию о реальной производительности алгоритма Карацубы, рассмотрим функции kmul() и ksqr(). Деление сомножителей на две части выполняется на месте, то есть копиро- вать их не нужно. А вот что нужно, так это, во-первых, снабдить сомножители указателями на младшие разряды и, во-вторых, де- лать это для каждого сомножителя отдельно (так как длины сомно- жителей могут различаться). H's :r O" ; f В качестве эксперимента мы реализовали смешанную функцию, объединившую в себе рекурсивную процедуру для умножения чисел, длина которых превышает некоторое число, определяемое макросом, и обычное умножение и возведение в степень для ма- леньких чисел. Для нерекурсивного умножения в функциях kmul() и ksqr() будем использовать вспомогательные функции miilt() и sqr(), в которых умножение и возведение в квадрат реализовано в виде базовых (kernel) функций, не поддерживающих тождествен- ные адреса аргументов (режим сумматора) и приведение по модулю в случае переполнения. Функция: Метод Карацубы умножения двух чисел а_1 и bj длиной 2к разря- дов в системе счисления с основанием В Синтаксис: void kmul(clint *aptrj, clint *bptr_l, int len_a, int len_b, CLINT pj); Вход: aptrj (указатель на младший разряд сомножителя а_1) bptrj (указатель на младший разряд сомножителя bj) 1еп_а (число разрядов сомножителя а_1) len_b (число разрядов сомножителя bj) Выход: pj (произведение) void kmul (clint *aptr_l, clint *bptr_l, int len_a, int len_b, CLINT pj)
ГЛАВА 4. Основные операции 61 CLINT cO1J, C10J; clint cOJ[CLINTMAXSHORT + 2]; clint d J[CLINTMAXSHORT + 2]; clint c2J [CLINTMAXSHORT + 2]; CLINTDtmpJ; г йфг clint *a1ptr_J, *b1ptrj; w int I2; > if ((len_a == len_b) && (len_a >= MUL_THRESHOLD) && (0 == (len_a & 1)) ) { Если оба сомножителя имеют одно и то же четное число разря- дов, превышающее значение MUL_THRESHOLD, то используем рекурсию, разбивая сомножители на две половины, младшим разрядам каждой из которых соответствуют указатели aptrj, а1 ptr_l, bptrj, Ы ptrJ. Эти половины мы не копируем и тем са- мым экономим время. Значения с0 и С] вычисляются рекурсив- ным вызовом функции kmul() и присваиваются переменным cOj и d J типа CLINT. I2 = len_a/2; alptrj = aptrj + 12; blptrj = bptrj + 12; kmul (aptrj, bptrj, 12,12, cOJ); kmul (alptrj, blptrj, 12,12, c1 J); При вычислении значения c2 = (a0 + а7)(Ьо + bj - c0 - Cj выполня- ем два сложения, один вызов функции kmul() и два вычитания. Аргументами вспомогательной функции addkar() являются указа- тели на младшие разряды и число разрядов двух слагаемых рав- ной длины, значением функции - сумма этих слагаемых, имею- щая тип CLINT. __ addkar (alptrj, aptrj, 12, cO1 J); addkar (blptrj, bptrj, 12, c10J);
62 Криптография на Си и C++ в действии kmul (LSDPTR_L (с01 J), LSDPTR_L (c10J),' DIGITSJ. (c01 J), DIGITSJ. (c10J), c2J); sub (c2J, c1 J, tmpj); sub (tmpj, cOJ, c2J); Выполнение функции заканчивается вычислением значения Bk(Bkd + с2) + с0. Для этого используем функцию shiftaddO, которая при сложении сдвигает влево первое из двух слагаемых типа CLINT на заданное число позиций в системе счисления с основанием В. shiftadd (d J, c2J, I2, tmpj); shiftadd (tmpj, cOJ, I2, pj); } Если хотя бы одно из входных условий не выполнено, прерываем рекурсию и вызываем нерекурсивную функцию умножения mult(). Для вызова функции mult() необходимо перевести части aptrj и bptrj в формат CLINT. д else { memcpy (LSDPTRJ. (c1 J), aptrj, len_a * sizeof (clint)); memcpy (LSDPTRJ. (c2J), bptrj, len_b * sizeof (clint)); SETDIGITS J. (c1J, len_a); SETDIGITSJ_ (c2J, lenjD); mult (c1 J, c2J, pj); RMLDZRS.L (pj); } } Возведение в квадрат методом Карацубы выполняется аналогично, поэтому не будем его описывать подробно. Для вызова функций kmul() и ksqr() используем функции kmuIJO и ksqrj(), имеющие стандартный интерфейс.
ГЛАВА 4. Основные операции 63 функция: Умножение и возведение в квадрат методом Карацубы Синтаксис: int kmulj (CLINT aj, CLINT b_l, CLINT pj); int ksqrj (CLINT aj CLINT pj); Вход: aj, bj (сомножители) Выход: pj (произведение или квадрат) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения Функции, реализующие алгоритмы Карацубы, читатель найдет в файле kmul.c на прилагаемом к книге компакт-диске. Многочисленные проверки этих функций (на Pentium III, 500 МГц, под Linux) показали, что наилучший результат достигается, когда нерекурсивная процедура умножения вызывается для не более чем 40-разрядных чисел (что соответствует 640 двоичным разрядам). Временные оценки для нашей программы приведены на рис. 4.3. Рисунок 4.3. Процессорное время умножения метолом К аранубы Из рисунка видно, что результаты действительно оправдали наши ожидания. Разница между обычным умножением и возведением в сте- пень составляет около 40%. Для чисел размером более 2000 двоич- ных разрядов время работы алгоритмов становится более заметным, лидирует по скорости алгоритм Карацубы. Интересный факт: «нор- мальное» возведение в квадрат sqr_l() значительно быстрее умно- жения Карацубы, а возведение в квадрат методом Карацубы ksqrJO лидирует только для чисел длиной более 3000 двоичных разрядов.
64 Криптография на Си и C++ в действии Функции, реализующие алгоритмы Карацубы для маленьких чисел, значительно ускорены по сравнению с первым изданием этой книги, но в них все еще есть что улучшать. Заметные скачки во времени работы функции kmulj() показывают, что рекурсия прерывается раньше, чем это определено пороговым значением, если длина сомножителей - нечетное число. В худшем случае это происходит в самом начале процедуры умножения, и даже для очень больших чисел нам не остается ничего лучшего, как применять обычное умножение. Как нам кажется, стоит обобщить функции, реализую- щие алгоритм Карацубы так, чтобы они могли обрабатывать аргу- менты разной (в том числе нечетной) длины. Дж. Зиглер (J. Ziegler) [Zieg] из Института Марка Планка (г. Саарбрюкен, Германия) разработал для 64-разрядного процессора (Sun Ultra-1) переносимую программу, которая реализует алгоритмы умножения и возведения в степень методом Карацубы и «обгоняет» обычные методы на числах длины 640 двоичных разрядов. Возве- дение в квадрат работает на 10% быстрее для 1024-битных чисел и на 23% - для 2048-битных. Еще раз отметим, что алгоритмы Карацубы в том виде, в каком они есть, не дают значительного преимущества для криптографиче- ских приложений, так что мы предпочитаем вернуться к традицион- ным функциям mulj() и sqrj() умножения и возведения в степень. Если для ваших приложений функции, реализующие алгоритмы Карацубы, подходят, просто воспользуйтесь ими, а не функциями muIJO и sqr_l(). 4.3. Деление с остатком - У тебя осталось что-нибудь в кармане? ; I - Нет, - отвечала Алиса грустно. - Только на- | переток. j Льюис Кэррол, Приключения Алисы в Стране Чудес, (Перевод с английского Н. Демуровой) Заложим в наше здание основных арифметических операций над большими числами последний камень - деление - наиболее труд- ную из всех операций. Поскольку мы работаем с натуральными числами, то и результат мы можем выражать только натуральными числами. Принцип деления, которым мы собираемся заняться, называется делением с остатком и основан на следующем соотношении. Для данных чисел a, b G 72L, Ь>0, существует единственная пара целых чисел q и г таких, что а = qb + г, где О < г < Ь. Будем называть q частным, а г остатком от деления а на Ь. Чаще всего нас интересует только остаток, так что о частном мож- но не беспокоиться. В главе 5 мы узнаем, как важно уметь вычис-
ГЛАВА 4. Основные операции 65 * t с: лять остаток - эта операция используется о многих алгоритмах, всегда одновременно со сложением, вычитанием, умножением и возведением в степень. Так что нам нужно постараться разработать как можно более быстрый алгоритм. Самый простой способ деления с остатком для натуральных чисел а и b - вычитать делитель b из делимого а, пока остаток г не будет меньше делителя. Подсчитав число вычитаний, мы получим част- ное. Частное q и остаток г равны: q = \_albJ и г = а - [_alb^b.3 f ? ’ Согласитесь, делить с остатком с помощью вычитания очень скучно. Даже в школьном методе деления «в столбик» используется значи- тельно более эффективный алгоритм: разряды частного определя- ются последовательно, делитель умножается на каждый из них, а полученные частичные произведения вычитаются из делимого. Пример работы этого алгоритма приведен на рис. 4.4. Рисунок 4.4. Схема деления с остатком 354938:427=831, остаток 101 - 3416\|/ = 01333 - 1281 ф = 00528 - 427 = 101 «So.. :S : Уже при вычислении первого разряда частного - 8 - нам нужно попытаться угадать его или определить методом проб и ошибок. Ошибка выявляется либо если произведение (разряда частного на делитель) слишком велико (в нашем примере - больше, чем 3549), либо если разность разрядов делимого и частичного произведения больше, чем делитель. В первом случае выбрано слишком большое число (разряд частного), во втором - слишком маленькое; как бы то ни было, придется его исправлять. При разработке программы эвристический образ действия следует заменить чем-нибудь более определенным. Попробуем же, вслед за Д. Кнутом (см. [Knut], п. 4.3.1), «отшлифовать» наши грубые вы- числения. Обратимся к нашему примеру. Пусть натуральные числа а = (am+n-\am+n_2- • -ао)в и Ь = (Ьп_1Ьп_2---Ьо)в представлены в системе счисления с основанием В и Ьп_\ > 0 (стар- ший разряд). Будем искать частное q и остаток г такие, что a - qb + г, где 0 < г < Ь. Действуя согласно методу деления «в столбик», на каждом шаге получаем значение qj.= \_Rlb\<B, где число R = (ат+п_\ат+п-2-•-вк)в образовано старшими разрядами делимого, а значение к выбрано из Заметим, что для а < 0, когда q = -Г\a\/b\ и r = b- (|а| + qb), если а ]( b ,и г = 0, если а | Ь, деление с остатком сводится к случаю a, b g IN.
66 Криптография на Си и C++ в действии условия 1 < [_R/bJ (в примере выше на первом шаге получаем т + н-1 = 3 + 3-1=5, & = 2, 7? = 3549). Далее полагаем R := R - qjb\ разряд qj частного определен правильно, если выполнено условие 0 < R < Ь. Теперь заменяем R на сумму RB + (следующий разряд де- лимого) и вычисляем следующий разряд частного как После того как пройдены все разряды делимого, процесс останавливается. Остаток от деления равен последнему найденному значению R. Чтобы запрограммировать эту процедуру, нам необходимо уметь для заданных больших чисел R = (гпгп^...го)в и b = (Ьп.\Ьп-ъ^Ь^в таких, что [.RibJ < В, находить частное Q := [_RlbJ (разряд гп может быть и нулевым). Воспользуемся аппроксимацией q для Q, вычис- ляемой по старшим разрядам числа В и В, из книги Кнута. Пусть (4.1) q := min- Г"--+ Г'1~1 , В -1 . IL J J Если bn_\ > [.RibJ, то q удовлетворяет двойному неравенству (см. [Knut], п. 4.3.1, Теоремы А и В): q -2<Q< q. В предположении, что старший разряд делителя достаточно велик по сравнению с В, аппроксимация q превышает истинное значение Q не больше чем на 2 и никогда не бывает слишком мала. Этого всегда можно добиться, «растянув» операнды а и Ь. Выберем число d > 0 так, чтобы dbn_\ > [_В12] и положим d:=ad = (am+ndm+n_x...do)B, b\=bd = (Ьп_{Ьп_2 ,,.bQ)в . Значение d выбираем так, чтобы число разрядов в b не превышало числа раз- рядов в Ь. При этом мы учли, что а может содержать на один раз- ряд больше, чем а (если это не так, полагаем ат+п = 0). Как бы то ни было, значение d лучше выбрать равным степени двойки, по- скольку в этом случае «растягивание» операндов осуществляется простым сдвигом. Так как оба операнда умножаются на одно и то же число, частное не изменится: ^А_|= LV^J- Прежде чем применять аппроксимацию q из формулы (4.1) к «рас- тянутым» операторам а (соответственно г) и b , уточним ее, чтобы получить q = Q или q = Q +1 : если для выбранного значения q вы- полняется неравенство bn_2q >(rnB + rn_x - qbn_x)B + гп_2 , то умень- шаем q на 1 и снова проверяем выполнение неравенства. Так мы отсеиваем все случаи, когда q превышает истинное значение на 2;
ГЛАВА 4. Основные операции 67 в очень редких случаях q будет превышать истинное значение на 1 (см. [Knut], п. 4.3.1, Упражнения 19, 20). Последняя ситуация выяв- ляется, когда мы вычитаем частичное произведение делителя на разряд частного из того, что осталось от делимого. В этом случае в последний раз уменьшаем q на 1 и корректируем остаток. Приве- дем теперь алгоритм деления с остатком. Алгоритм деления с остатком числа а = (ат+п^ат+п^»»ао)в > 0 на число b = ^о)в > 0 1. Определить множитель d как описано выше. 2. ПОЛОЖИТЬ Г .— (.^m+n^m+n-i^m+n-2' • «Го)/? • ^о)в* 3. Положить i<— т + n,j <— т. 4. Положить q<r- minJ rjB+rj_l Vl , В -1 k где q, Г;Ч, - разряды ПЧ ЯГ г* 5. соответствующих векторов, умноженных на d (см. выше). Если bn_2q > (J]В + Г/-!-qbn_x)B + rz_2 , то положить q^-q-1 и по- вторить проверку. Если г - bq < 0, то положить q <— q -1. • - A.R •, 6. Положить г := (г/^.. -Г1-п)в - bq И qj <-q . К/’’ . ' v - л- .W 7. 8. Положить i <— i - 1 и j <— j - 1. При i > п вернуться на шаг 4. Результат: q = (,qmqm-\.. .qo)B и г = (r„-ir„_2...r0)B. Если делитель состоит всего из одного разряда то процедуру можно сократить, задав в самом начале г <— 0 и поделив двухразряд- ное число (га,)в с остатком на Ь$. Тогда в г записывается остаток: г <- (га,)в - a cq пробегает все разряды делимого. По окончании процедуры остаток будет равен г, а частное - q = (qmqm-\.. .<?о)в- Теперь, когда у нас есть все необходимое для реализации деления, напишем на языке С функцию, соответствующую рассмотренному выше алгоритму. Функция: Синтаксис: Вход: Выход: Возврат: Деление с остатком int divj (CLINT d1 J, CLINT d2J, CLINT quotj, CLINT remJ); d1 J (делимое), d2J (делитель) quotj (частное), remJ (остаток) E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0
Криптография на Си и C++ в действи! int divj (CLINT d1 J, CLINT d2J, CLINT quotj, CLINT remJ) { register clint *rptrj, *bptrj; CLINT bj; /* Допускаем остаток двойной длины плюс 1 разряд */ clint rj[2 + (CLINTMAXDIGIT « 1)]; clint *qptrj, *msdptrbj, *lsdptrrj, *msdptrrj; 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; Присваиваем значения делимого a = (ат+п_1ат+п_2...а0)в и делителя b = (Ьп_|Ьп_2...Ь0)в переменным rj и bj типа CLINT. Отбрасываем все ведушие нули. Если при этом делитель равен нулю, то завер- шаем функцию с кодом ошибки E_CLINT_DBZ. Длина делимого может достигать удвоенного числа разрядов, заданного в МАХВ. Позже это позволит нам использовать деление в функциях модульной арифметики. Для хранения частного двойной длины следует выделить память, которая всегда должна быть доступна. cpyj (r_I, d1 J); cpyj (bj, d2J); if (EQZ_L (bj)) return E_CLINT_DBZ; Проверяем тривиальные ситуации: делимое = 0, делимое меньше делителя или делимое равно делителю. В любом из этих случаев завершаем процедуру. if (EQZ_L (rj)) { SETZERO J_ (quotj); SETZERO J_ (remJ); return E-CLINTJDK;
ГЛАВА 4. Основные операции 69 i = cmpj (r_l, bj); if (i ==-1) { cpyj (remJ, rJ); SETZERO J. (quotj); return E_CLINT_OK; } else if (i == 0) SETONE_L (quotj); SETZERO-L (remJ); return E_CLINT_OK; На следующем шаге проверяем, не состоит ли делитель всего из одного разряда. В этом случае строим ветвь более быстрого де- ления, которую мы опишем позже. if (DIGITSJ. (bj) == 1) goto shortdiv; Теперь начинаем собственно деление. Сначала задаем множитель d как степень двойки. Пока bn_| > BASEDIV2 := L.B/2J, сдвигаем \ старший бит Ьп_| делителя влево на один бит, при этом увеличи- \ J ваем d каждый раз на 1 (начинаем с d = 0). Затем устанавливаем У указатель msdptrbj на старший разряд делителя. Впоследствии мы будем часто пользоваться значением BITPERDGT-d, так что запишем его в переменную sbitsminusd. msdptrbj = MSDPTRJ. (bj); bn = *msdptrbj; while (bn < BASEDIV2) { d++; bn «= 1;
70 Криптография на Си и C++ в действии } sbitsminusd = (int)(BITPERDGT - d); Если d > 0, то вычисляем два старших разряда bn_xbn_2 числа db и записываем их в bn и Ьп_1. Здесь следует различать случаи, когда делитель b имеет ровно два разряда и больше чем два раз- ряда. В первом случае справа в Ьп_2 дописываем двоичные нули, во втором случае младшими битами числа Ьп_2 становятся биты числа Ьп.3. к if (d > 0) { bn += *(msdptrbj - 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-векторе г J, куда будем записывать остаток от деления, устанавливаем указатели msdptrrj и IsdptrrJ на старший и млад- ший разряды числа (am+nam+n_1...am+1)B соответственно. Разряд am+n переменной г_1 полагаем равным 0. Устанавливаем указатель qptrj на старший разряд частного. msdptrbj = MSDPTR_L (bj); msdptrrj = MSDPTRJ. (rj) + 1; IsdptrrJ = MSDPTRJ. (rj) - DIGITS_L (bj) + 1;
ГЛАВА 4. Основные операции 71 *msdptrrj = 0; qptrj = quotj + DIGITS J_ (rj) - DIGITS_L (bj) + 1 ; Переходим к основному циклу. Указатель IsdptrrJ пробегает раз- ряды ат, ат_2, а0 делимого в rj, а (неявный) индекс i - значе- ния i = т + п, п. while (IsdptrrJ >= LSDPTFLL (гJ)) { Готовимся к вычислению q . Умножаем три старших разряда час- ти (aiai_1...ai_n)B делимого на d и присваиваем полученные значе- ния переменным ri, rij и ri_2. Отдельно рассматриваем случай, когда указанная часть делимого состоит ровно из трех разрядов. При первом проходе цикла имеем как минимум три разряда: в предположении, что сам делитель b состоит как минимум из двух разрядов, существуют старшие разряды am+n_| и am+n_2 делимого, а разряд am+n мы положили равным нулю при инициализации век- тора rj. ri = (USHORT)((*msdptrrJ « d) + (*(msdptrrj - 1) » sbitsminusd)); nJ = (USHORT)((*(msdptrrJ - 1) « d) + (*(msdptrrj - 2) » sbitsminusd)); if (msdptrrj - 3 > rj) /* Четыре разряда делимого */ { ri_2 = (USHORT)((*(msdptrrJ - 2) « d) + (*(msdptrrj - 3) » sbitsminusd)); } else /* Только три разряда делимого */ { ri_2 = (USHORT)(*(msdptrrJ - 2) « d); } Теперь дело дошло до вычисления аппроксимации qf которой соответствует переменная qhat. Будем различать случаи ri Ф bn (частый) и ri = bn (редкий).
72 Криптография на Си и C++ в действии ..-WMRiW- 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)bnJ * 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;
операции 73 right = ((ULONG)(rhat = (ULONG)bn + (ULONG)ri_1) « BITPERDGT) + ri_2; if (rhat < BASE) { if ((left = (ULONG)bn_1 * qhat) > right) { qhat-; if ((rhat + bn) < BASE) { if ((left - bn_1) > (right + ((ULONG)bn « BITPERDGT))) { qhat-; } Вычитаем произведение qhat • b из части u := (aiai_1... a^Je дели- мого, которая заменяется полученной разностью. Продолжаем умножение и вычитание, сдвигаясь каждый раз на один разряд. Здесь нужно помнить вот о чем. Произведение qhat • Ь, может быть и двухразрядным. Оба разряда до поры до времени хранятся в переменной carry типа ULONG. Старший разряд переменной carry рассматривается как перенос при вычитании следующего по старшинству разряда. В том случае, когда разность u - qhat • b отрицательна (то есть значение qhat больше истинного на 1), следует вычислить значе- ние uz := Bn+1 +и - qhat • b и рассматривать результат по модулю Bn+1 как В-дополнение й значения и. После вычитания старший разряд u'i+1 числа и' записываем в старшее слово переменной borrow типа ULONG. Неравенство u'i+i 0 в точности означает, что значение qhat слишком велико. В этом случае исправляем результат, вычисляя сумму u <—u'+ b по модулю Bn+1. Случай корректировки рас- смотрим несколько позже. borrow = BASE; carry = 0;
74 Криптография на Си и С-ь+ в действии for (bptrj = LSDPTFLL (bj), rptrj = IsdptrrJ; bptrj <= msdptrbj; bptrj++, rptrj++) { if (borrow >= BASE) { *rptrj = (USHORT)(borrow = ((ULONG)*rptrJ + BASE - (ULONG)(USHORT)(carry = (ULONGfbptrJ * qhat + (ULONG)(USHORT)(carry » BITPERDGT)))); } else ‘rptrj = (USHORT)(borrow = ((ULONG)‘rptrJ + BASEMINONEL- (ULONG)(USHORT)(carry = (ULONG)'bptr_l * qhal + | (ULONG)(USHORT)(carry » BITPERDGT)))); } 1 } if (borrow >= BASE) j { I *rptrj = (USHORT)(borrow = ((ULONG)*rptrJ + BASE - (ULONG)(USHORT)(carry » BITPERDGT))); } else *rptrj = (USHORT)(borrow = ((ULONG)*rptrJ + BASEMINONEL - | (ULONG)(USHORT)(carry » BITPERDGT))); Запоминаем разряд частного на случай если понадобится кор- ректировка. rqptrJ = qhat;
ГЛАВА 4. Основные операции 75 Как и было обешано, проверяем, не превышает ли разряд частно- го истинное значение на 1. Этот случай встречается чрезвычайно редко (ниже мы введем для него специальную проверку) и прояв- ляется в том, что старшее слово переменной borrow типа ULONG равно нулю, то есть borrow < BASE. Тогда нужно положить и <— u' + b по модулю Bn+1 (см. выше). if (borrow < BASE) carry = 0; for (bptrj = LSDPTRJ. (bj), rptrj = IsdptrrJ; bptrj <= msdptrbj; bptrj++, rptrj++) { *rptrj = (USHORT)(carry = ((ULONG)*rptrJ + (ULONG) (*bptrj) + (ULONG)(USHORT)(carry » BITPERDGT))); } *rptrj += (USHORT)(carry » BITPERDGT); (*qptrj)-; } Устанавливаем указатели на остаток и частное и возвращаемся к началу основного никла. msdptrrj--; IsdptrrJ-; qptrj-; Определяем длины частного и остатка. Число разрядов частного может не больше чем на 1 превышать разность между числом разрядов делимого и делителя. Длина остатка не может превы- шать длины делителя. В обоих случаях определяем истинную длину, отбрасывая все ведущие нули.
76 Криптография на Си и C++ в действии SETDIGITSJ. (quotj, DIG ITS J_ (rj) - DIGITS J_ (bj) + 1); RMLDZRSJ. (quotj); SETDIGITSJ. (rj, DIGITSJ_ (bJ)); cpyj (remJ, rj); return E_CLINT_OK; В случае «короткого деления» делитель состоит всего из одного разряда Ьо, на который делится двухразрядное число (raj)B, где а; пробегает все разряды делимого. Устанавливаем начальное зна- чение остатка: г <— 0, а затем полагаем равным разности г <— (ra;)B - qb0. Значение г представлено переменной rv типа USHORT, значение (raj)B - переменной rhat типа ULONG. shortdiv: rv = 0; bv = *LSDPTR_L (bj); for (rptrj = MSDPTRJ. (rj), qptrj = quotj + DIGITS J. (rj); rptrj >= LSDPTR_L (rj); rptrj-, qptrj-) { *qptrj = (USHORT)((rhat = ((((ULONG)rv) « BITPERDGT) + (ULONG)*rptrJ)) I bv); rv = (USHORT)(rhat - (ULONG)bv * (ULONG)*qptrJ); } SETDIGITSJ. (quotj, DIGITS J. (rJ)); RMLDZRSJ. (quotj); u2clintj (remJ, rv); return E_CLINT_OK; Время t = O(mn) работы функции деления то же, что и для умно* жения, где тип- числа разрядов соответственно делимого и дели- теля в системе счисления с основанием В.
ГЛАВА 4. Основные операции 77 Теперь мы хотим представить на суд читателя несколько разновид- ностей алгоритмов деления с остатком, основанных на только что рассмотренной универсальной функции. Прежде всего, введем смешанную версию деления, когда делимое имеет тип CLINT, а делитель - тип USHORT. Для этого обратимся к подпрограмме для делителей малой длины функции div_l(). В ней практически ничего менять не надо, поэтому приведем только интерфейс. функция: Деление переменной типа CLINT на переменную типа USHORT Синтаксис: int udivj (CLINT dvj, USHORT uds, CLINT qj,CLINT rj); Вход: dv_l (делимое), uds (делитель) Выход: qj (частное), rj (остаток) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 Как уже отмечалось, в ряде случаев вычислять частное не нужно, а интересен только остаток. Это не дает большой экономии времени, но, по крайней мере, таскать указатель к ячейке, где хранится част- ное, не имеет смысла. Тогда логично было бы написать самостоя- тельную функцию для вычисления остатков, или «вычетов». Мате- матическую подоплеку использования этой функции нам предстоит подробно изучить в главе 5. Функция: Вычисление остатка (вычета по модулю п) Синтаксис: int modj (CLINT dj, CLINT nJ, CLINT rj); Вход: dj (делимое), nJ (делитель или модуль) Выход: rJ (остаток) Результат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 Остаток вычисляется значительно проще, если модуль равен сте- пени двойки, а именно 2к, так что и для этого случая сгодится своя собственная функция. Остаток делимого от деления на 2 полу- чается отбрасыванием всех двоичных разрядов после к-го, при этом отсчет начинается с 0. Такое отбрасывание соответствует побитной логической операции AND (см. п. 7.2) делимого и числа 2*-1= (111111... 1)2, состоящего из к двоичных единиц. Самым важным объектом при выполнении этой операция является тот раз- ряд делимого, представленного в системе счисления с основанием В, который содержит к-й бит. Все более старшие разряды делимого нас не интересуют. Делитель в соответствующей функции modj() представлен только показателем к.
78 Криптография на Си и C++ в действии функция: Вычисление остатка от деления на степень двойки (вычисление вычета по модулю 2*) Синтаксис: int mod2J (CLINT dj, ULONG k, CLINT rj); Вход: dj (делимое), k (показатель степени делителя или модуля) Возврат: г_1 (остаток) .. 4 int mod2J (CLINT dj, ULONG k, CLINT rj) { int i; ” Lot" •**;'*м { | Поскольку 2k > 0, проверять случай деления на 0 не нужно. Сна- чала копируем значение dj в rj. Если к превышает максималь- ную двоичную длину, допускаемую типом CLINT, то завершаем процедуру. cpyj (rj, dJ); .si / i hO : , > ' * ’ if if (k > CLINTMAXBIT) return E_CLINT_OK; I § ,f 1 Определяем тот разряд переменной rj, в котором нужно что-то менять, и присваиваем его номер переменной i. Если значение i превышает число разрядов в г J, то завершаем процедуру. i . ; • i = 1 + (k » LDBITPERDGT); if (i > DIGITSJ_ (rj)) return E_CLINT_OK; Теперь применяем логическую операцию AND к разряду пере- I I менной г J (отсчитываем с 1), определенному на предыдущем шаге, и значению 2kmod BITPERDGT _ 1 (= 2kmod16_ 1 в нашей реализации). Новое значение i числа разрядов переменной rj запоминаем в rJ[0]. Удаляем нулевые старшие разряды и получаем результат. гJ[i] &= (1U « (к & (BITPERDGT — 1))) — 1U; SETDIGITSJ. (rj, i); RMLDZRSJ. (rj);
ГЛАВА 4. Основные операции__________ ______________ 79 —— return E-CLINTJDK; } В смешанном варианте функции вычисления вычетов делитель имеет тип USHORT, остаток тоже представляется типом USHORT. Здесь мы опять приводим только интерфейс; сами функции чита- тель сможет найти в исходных текстах пакета FLINT/C. функция: Вычисление остатка, деление переменной типа CLINT на перемен- ную типа USHORT Синтаксис: USHORT umodj (CLINT dvj, USHORT uds); Вход: dvj (делимое), uds (делитель) Возврат: > неотрицательный остаток, если все в порядке OxFFFF в случае деления на 0 При тестировании программ, реализующих деление, - да и любых других программ вообще, - следует учесть некоторые моменты (см. главу 12). В частности, шаг 5 нужно проверять подробно, по- скольку для случайно выбранных контрольных значений он выпол- няется лишь с вероятностью 2/В (= 2“15 в нашем случае) (см. [Knut], п. 4.3.1, Упражнение 21). Следующие контрольные значения делимого а и делителя b (с уже вычисленными частным q и остатком г) подобраны так, что та часть программы, которая реализует шаг 5, выполняется дважды. Еще несколько таких контрольных значений читатель найдет в тестовой программе testdiv.c. Контрольные значения даны в шестнадцатиричном виде, разряды идут справа налево в порядке возрастания, длина не указана. Контрольные значения для шага 5 алгоритма деления а = еЗ 7d За Ьс 90 4b ab а7 а2 ас 4b 6d 8f 78 2b 2b f8 49 19 d2 91 73 47 69 Od 9e 93 de 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 Ы ff ff f4 fe 5c 0e27 23 r = ca 23 12 fb b3 f4 c2 3a dd 76 55 e9 4c 34 10 Ы 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. Модульная арифметика: вычисление в классах вычетов - А вы можете исчезать и появляться не так внезапно? А то у меня голова идет кругом. - Хорошо, - сказал Кот и исчез - на этот раз п: очень медленно. Первым исчез кончик его хвоста, а последней - улыбка; она долго парила в воздухе, когда все остальное уже пропало. - Д-да! - подумала Алиса. - Видала я котов без улыбок, но улыбка без кота! Льюис Кэррол, . Приключения Алисы в Стране Чудес Начнем эту главу с основных правил деления с остатком. Попы- таемся объяснить важность деления с остатком, его возможные v приложения и способы вычисления. Ну а для начала - немного 4. алгебры, чтобы читатель смог понять те функции, которые мы вве- t. дем позже. Мы уже знаем, что при делении с остатком целого числа ае Z на натуральное число 0 < т g DN существует единственное представ- ление а = qm + г, 0 < г < т. Число г называется остатком от деления а на т, или вычетом по модулю т. При этом число т делит разность а - г, обозначается как т | (а - г). К. Гаусс ввел для этого соотношения другое обозначение: 1 а = г mod т (читается «а сравнимо с г по модулю т»). Сравнимость по модулю натурального числа т является отноше- нием эквивалентности на множестве целых чисел. Это означает, что множество R:={(a,b)\ a = b mod т} пар целых чисел таких, что т | (д - /?), обладает следующими свойствами: (a) Rрефлексивно', для любого целого числа а пара (д, д) лежит в R; то есть а = a mod т. Карл Фридрих Гаусс (1777-1855) - один из величайших математиков всех времен. Сделал мно- жество важных открытий в математике и естественных науках. В возрасте 24 лет опубликовал зна- менитую работу Disquisitiones Arithmeticae, послужившую основой для современной теории чисел.
82 Криптография на Си и C++ в действии (б) R симметрично', если (a, b) G R, то (Z?, a) 6 R', то есть из а = b mod т следует, что b = a mod т. (в) R транзитивно', если (a, Z?) g R, (/>, с) е R, то (я, с) G 7?; то есть из а = b mod т и b = с mod т, следует, что а = с mod т. Доказательство этих свойств следует непосредственно из опреде- 6’ ^ ления операции деления с остатком. Отношение эквивалентности R делит множество целых чисел на непересекающиеся подмножества, называемые классами эквивалентности', для данного остатка г и натурального числа т > 0 множество г := {а | а = г mod ш}, или, в других обозначениях, r + ш^, называется классом вычетов числа г по модулю т. Элементами этого класса являются все целые числа, дающие при делении на т один и тот же остаток г. ' Например, пусть ш = 7, г =5; тогда множество целых чисел, даю- щих при делении на 7 остаток 5, - это класс вычетов 5 = 5 + 7 -2 = {...,-9,-2,5, 12, 19, 26, 33, ...}. Два класса вычетов по модулю фиксированного числа т либо совпадают, либо не пересекаются.2 Следовательно, класс вычетов однозначно определяется любым из своих элементов. Элементы класса вычетов называются представителями', любой элемент может служить представителем класса. Равенство классов вычетов эквивалентно сравнимости представителей этих классов по данному модулю. Поскольку при делении с остатком остаток всегда меньше делителя, для любого целого т существует конечное число классов вычетов по модулю т. Теперь мы, наконец, вплотную приблизились к сути наших рассу- ждений. Классы вычетов - это такие объекты, в которых можно выполнять арифметические действия, оперируя лишь с представи- * телями. Вычисления в классах вычетов играют огромную роль в алгебре и теории чисел, а значит, незаменимы в теории кодирова- ния и современной криптографии. Далее мы попытаемся пояснить v fCi алгебраические аспекты модульной арифметики. ’ ' Пусть а, Ъ и т - целые числа, т > 0. Для классов вычетов а и b по ' модулю т определим операции «+» и «•», которые назовем сложени- ем и умножением (классов вычетов), поскольку определяются они по аналогии с соответствующими операциями над целыми числами: а + b := а + b (сумма классов равна классу суммы); а • Ъ := а • b (произведение классов равно классу произведения). 2 Множества называются непересекающнлшся, если у них нет общих элементов или, иначе, если их пересечение есть пустое множество.
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов 83 Обе операции определены корректно, поскольку в обоих случаях результат является классом вычетов по модулю т. Множество 7Lm := { г | г - вычет по модулю т} классов вычетов по модулю т, на котором определены эти две операции, называется конечным коммутативным кольцом +, •) с единицей, что, в частности, подразумевает выполнение следующих аксиом: (а) Замкнутость по сложению: Сумма двух элементов множества является элементом множе- ства (б) Ассоциативность сложения: Для любых а, Ь, с из справедливо а + (6 + с) = (а + й) + с . (в) Существование нулевого элемента: Для любого а из справедливо а + 0 = а . (г) Существование противоположного элемента: Для любого а из существует единственный элемент b из та- кой, что а + b = 0 . (д) Коммутативность сложения: Для любых а, b из Жт справедливо а + b = b + а . (е) Замкнутость по умножению: Произведение двух элементов множества является элементов множества (ж) Ассоциативность умножения: Для любых а, Ь, с из справедливо a-(b-c) = (а-Ь)-с. (з) Существование единичного элемента: Для любого а из справедливо а • 1 = а . (и) Коммутативность умножения: Для любых а, b из справедливо а • b = b • а . (к) В кольце +, •) выполняется закон дистрибутивности'. а-(Ь +с) = а‘Ь + а-с. На основании свойств 1-5 можно заключить, что множество (Жт, +) является абелевой группой, где термин «абелева» означает комму- тативность сложения. Свойство 4 позволяет определить на множе- стве операцию вычитания как сложение с противоположным элементом: если элемент с является противоположным к Ь, то b + с = 0, а значит, для любого а е ~ZLm а - b := а + с.
84 Криптография на Си и C++ в действии •’ Ж-'* На множестве (Z„2, •) справедливы групповые законы 6-9 для опе- рации умножения, единицей является элемент 1. Однако не для каждого элемента множества Жт обязательно существует обратный, то есть (Z„2, •), вообще говоря, является не группой, а лишь комму- тативной полугруппой с единицей.3 Но если исключить из все элементы, не взаимно простые с т (в том числе и 0), то полученная структура будет абелевой группой по умножению (см. п. 10.2). Обозначим ее через (Z/zl*, •)• Значимость алгебраических структур, аналогичных группе (Zw*, •), можно пояснить на примере некоторых хорошо известных комму- тативных колец. Множество Z целых чисел, множество Q рацио- нальных чисел и множество [R вещественных чисел - все это ком- мутативные кольца с единицей, которые, в отличие от (Z„2 , •), бес- конечны (на самом деле, множество вещественных чисел является полем, то есть обладает некоторыми дополнительными свойствами). Все арифметические правила, приведенные выше для конечного кольца, хорошо нам известны - ведь мы пользуемся ими каждый -ВТ у ЛОТ . : день. В главе 12 они будут нам верными помощниками, когда при- дет время тестировать арифметические функции. А пока соберем о них важную информацию. При вычислении в классах вычетов мы оперируем исключительно с представителями этих классов. Из каждого класса вычетов по мо- дулю т выбираем ровно по одному представителю и получаем таким образом полную систему вычетов, в рамках которой и будем прово- дить все вычисления. Система наименьших неотрицательных выче- тов по модулю т представляет собой множество Rm := {0, 1, ..., ш-1}. Множество чисел г, удовлетворяющих неравенству ~т<г<±т, будем называть системой абсолютно наименьших вычетов по модулю т. В качестве примера рассмотрим кольцо Z26 = { 0,1,..., 25 }. Систе- мой наименьших неотрицательных вычетов по модулю 26 будет множество 7?2б={0, 1» 25}, системой абсолютно наименьших вычетов - множество {-12, -И, ..., 0, 1, ..., 13}. Поясним связь между арифметикой в классах вычетов и модульной арифметикой в системах вычетов: равенство 18 + 24 = 18 + 24 = 16 эквивалентно сравнению 18 + 24 = 42 = 16 mod 26; 3 Множество (Н, операция *. *) является полугруппой, если на множестве Н определена ассоциативная
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов 85 равенство 9-15 = 9 + 11 = 20 эквивалентно сравнению 9- 15 = 9 + 11 = 20 mod 26. Сопоставляя английскому алфавиту кольцо классов вычетов Z26 или множеству ASCII-символов кольцо Z256, можно производить вычисления с символами. Юлию Цезарю приписывают простей- шую систему шифрования, в которой каждая буква открытого тек- ста складывается по модулю 26 с некоторым фиксированным эле- ментом кольца Z26 (у Цезаря это был элемент 3). Таким образом, каждая буква алфавита сдвигалась на три позиции вправо, при этом X переходил в A, Y в В и Z в С. 4 Для вычислений в классах вычетов можно составить таблицы сло- жения и умножения. Покажем, как это делать на примере кольца Z5 (таблицы 5.1 и 5.2 соответственно). Таблииа 5.1. + 0 1 2 3 4 Таблииа сложения 0 0 1 2 3 4 по модулю 5 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. 1 2 3 4 Таблииа 1 1 1 3 4 умножения по модулю 5 2 2 4 1 3 3 3 1 4 2 4 4 3 2 1 То, что множество классов вычетов является конечным, дает нам значительные преимущества по сравнению с такими бесконечными структурами, как кольцо целых чисел, поскольку при выполнении арифметических операций в компьютерной программе у нас нико- гда не возникнет переполнения, если только будут выбраны подхо- дящие представители классов вычетов. Операция обработки ре- зультата, например функцией mod_l(), называется приведением Aulus Gellius, XII, 9 и Suetonius, Caes. LVI.
Криптография на Си и C++ в действии (по модулю /и). Теперь мы можем вычислять сколько душе угодно, ограничив лишь представление чисел и функций пакета FLINT/C полной системой вычетов по модулю т, где т < 7Vmax. Будем всегда оперировать с положительными представителями и работать в рам- ках системы неотрицательных вычетов. Свойства классов вычетов позволяют нам работать в пакете FLINT/C с большими числами ти- па CLINT. Трудности могут возникать лишь в отдельных ситуациях, которые мы специально обсудим. Довольно теории об арифметике в классах вычетов. Займемся те- перь функциями, реализующими модульную арифметику. Сначала вспомним функции modj() и mod2J() из п. 4.3, вычислявшие оста- ток от деления на т и 2к соответственно, а затем перейдем к функ- циям модульного сложения, вычитания, умножения и возведения в квадрат. Модульному возведению в степень, как особенно сложной теме, будет посвящена отдельная глава. Для удобства в обозначении класса вычетов будем опускать черту и вместо а писать а. Принцип работы функций модульной арифметики заключается в следующем: к операндам применяется соответствующая обычная («немодульная») функция, а затем результат делим с остатком на модуль. Следует, однако, отметить, что размер промежуточных результатов может достигать 2МАХВ разрядов, а в случае вычита- ния могут появляться отрицательные числа, что недопустимо в типе CLINT. Ранее мы назвали такие ситуации переполнением и потерей значащих разрядов соответственно. В основных арифметических функциях предусмотрен механизм обработки этих ситуаций: про- межуточные результаты рассматриваются как вычеты по модулю (Mnax + 1) (см. главы 3 и 4). Этот же метод можно применять и сей- час, в случаях, когда конечный результат модульной операции имеет тип CLINT. Чтобы получить верный результат в случаях перепол- нения и потери значащих разрядов, позаимствуем из уже рассмотренных нами в главе 4 функций базовые функции void add (CLINT, CLINT, CLINT); 1 void sub (CLINT, CLINT, CLINT); I void mult (CLINT, CLINT, CLINT); ; void umul (CLINT, USHORT, CLINT); ; void sqr (CLINT, CLINT); | Эти функции, выделенные из функций addj(), sub_l(), muIJO # sqr_J(), с которыми мы работали раньше, служат для выполнения собственно арифметических операций. Остальные операции: уда- ление старших нулевых разрядов, заполнение сумматора и обра- ботка возможного переполнения или потери значащих разрядов - все, что осталось на долю ранее введенных функций. Их синтаксис
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов 87 и семантика не изменяются, то есть мы по-прежнему можем ими пользоваться. Рассмотрим это преобразование на примере функции умножения mulj() (сравните с реализацией этой же функции на стр. 49). функция: Умножение Синтаксис: int mulj (CLINT f1 J, CLINT f2J, CLINT ppj); Вход: f1 J, f2J (сомножители) Выход: ppj (произведение) Возврат: E-CLINTJDK, если все в порядке E_CLINT_OFL в случае переполнения int mulj (CLINT f 1 J, CLINT f2J, CLINT ppj) { CLINT aaj, bbj; CLINTD pj; int OFL = 0; Удаление ведущих нулей и заполнение сумматора. cpyj (aaj, f1 J); cpyj (bbj, f2J); Вызов базовой функции умножения. mult (aaj, bbj, pj); Обработка переполнения, если таковое имеется. if (DIGITSJ. (pj) > (USHORT)CLINTMAXDIGIT) /* Переполнение ? 7
88 Криптография на Си и C++ в действии г ANDMAX_L (р_1); /* Привести по модулю (Nmax +1)7 OFL = E_CLINT_OFL; '9Ж. I'b 1 } cpyj (ppj, pj); jV r^W<**** *«*’*»*’ *» 9 ’*^f i » return OFL; } Аналогично изменяем и остальные функции: addj(), sub_l() и sqr_l(). Сами по себе базовые арифметические функции не содержат новых компонентов и поэтому здесь не приводятся; подробнее см. реали- зацию на flint .с. Базовые функции не вызывают переполнения, поэтому приведение i»< «»«'«» «»>» '< ’ ' по модулю (Атах + 1) в них не выполняется. Они являются внутрен- ними компонентами функций пакета FLINT/C и поэтому имеют описатель static. Работая с базовыми функциями, следует помнить, что они не могут оперировать с числами с ведущими нулями и что их нельзя использовать в режиме сумматора (см. главу 3). Функция sub() предполагает, что разность положительна. В про- тивном случае результат не определен, поскольку такая ситуация функцией sub() не предусмотрена. И наконец, при вызове базовых функций следует выделить пространство под промежуточные ре- ’ t‘ зультаты большой длины. В частности, для представления резуль- *тата функции sub() требуется по крайней мере столько же памяти, сколько для представления уменьшаемого. Что ж, теперь у нас есть все, чтобы разработать основные функции W4 -1 t модульной арифметики: madd_l(), msubj(), mmul_l() и msqr_l(). Функция: Модульное сложение Синтаксис: int maddj (CLINT aaj, CLINT bbj, CLINT cj, CLINT mJ); Вход: aaj, bbj (слагаемые), mJ (модуль) Выход: cj (остаток) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 int maddj (CLINT aaj, CLINT bbj, CLINT cj, CLINT mJ) { CLINT aj, bj; ( clint tmpJ(CLINTMAXSHORT + 1];
рддВА 5. Модульная арифметика: вычисление в классах вычетов if (EQZ_L (mJ)) { return E_CLINT_DBZ; } cpyj (aj, aaj); r cpyj (bj, bbj); sr- if (GE_L (aj, mJ) || GE_L (bj, mJ)) { add (aj, bj, tmpj); mod J (tmpj, mJ, cj); } else ] Если обе переменных aj и bj меньше модуля mJ, то делить с остатком не нужно. { add (aj, bj, tmpj); if (GE_L (tmpj, mJ)) subj (tmpj, mJ, tmpj); /* Исключаем потерю значащих разрядов */ В предыдущем вызове функции subJO мы немного подстрахова- лись, введя переменную tmpj. Эта переменная, где лежит сумма переменных aj и bj, может лишь на один разряд превышать константу МАХВ. Внутри функции subJO никаких нарушений быть не должно, поскольку память для хранения дополнительного разряда мы выделили. Таким образом, результат мы записываем 8 tmp J, з не сразу в cj, как можно было бы ожидать. Зато после выполнения функции subJO переменная tmpj у нас имеет не больше МАХВ разрядов.
90 Криптография на Си и C++ в действии cpyj (cj, tmpj); } return E_CLINT_OK; } I Функция модульного вычитания msubJO оперирует только с неот- рицательными промежуточными результатами функций addj(), subj() и modj(), то есть мы не выходим за рамки системы наи- меньших неотрицательных вычетов. Функция: Модульное вычитание Синтаксис: int msubj (CLINT aaj, CLINT bbj, CLINT cj, CLINT mJ); Вход: aaj (уменьшаемое), bbj (вычитаемое), mJ (модуль) Выход: cj (остаток) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 int msubj (CLINT aaj, CLINT bbj, CLINT cj, CLINT mJ) { CLINT aj, bj, tmpj; if (EQZ_L (mJ)) { return E_CLINT_DBZ; } cpyj (aj, aaj); cpyj (bj, bbj); Будем различать случаи aj > bj и aj < bj. В первом случае поступаем как обычно; во втором случае вычисляем разность (bJ - aj), приводим ее по модулю mJ и вычитаем полученное положительное число из mJ. if (GE_L (aj, bJ)) /* aj - bj > 0 7
91 j-ддВА 5. Модульная арифметика: вычисление в классах вычетов sub (aj, bj, tmpj); mod J (tmpj, mJ, cj); } else /* a J - b J < 0 7 { sub (bj, aj, tmpj); modj (tmpj, mJ, tmpj); if (GTZ_L (tmpj)) { sub (mJ, tmpj, cj); } else { SETZERO J. (cj); } } return E_CLINT_OK; } Теперь перейдем к функциям mmul_l() и msqr_l() модульного умно- жения и возведения в квадрат. Функция: Модульное умножение Синтаксис: int mmulj (CLINT aaj, CLINT bbj, CLINT cj, CLINT mJ); Вход: aaj, bbj (сомножители), mJ (модуль) Выход: cj (остаток) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 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; } cpyj (aj, aaj); cpyj (bj, bbj); mult (aj, bj, tmpj); mod J (tmpj, mJ, cj); return E_CLINT_OK; } Функция модульного возведения в квадрат строится аналогично, поэтому для нее приведем только интерфейс. Функция: Модульное возведение в квадрат Синтаксис: int msqrJ(CLINT aaj, CLINT cj, CLINT mJ); Вход: aaj (множитель), mJ (модуль) Выход: cj (остаток) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 Для каждой из этих функций (разумеется, за исключением возведе- ния в квадрат) можно определить соответствующую смешанную функцию, у которой второй аргумент имеет тип USHORT. Пока- жем, как это делается, на примере функции umaddj(). Функции umsubJO и ummuIJO строятся по образу и подобию, так что их приводить не будем. Функция: Модульное сложение переменных типа CLINT и USHORT Синтаксис: int umaddj (CLINT aj, USHORT b, CLINT cj, CLINT mJ); Вход: aj, b (слагаемые), mJ (модуль) Выход: cj (остаток) Возврат: E_CLINT_OK, если все в порядке E-CLINTJDBZ в случае деления на 0 int umaddj (CLINT aj, USHORT b, CLINT cj, CLINT mJ) { int err; CLINT tmpj;
ГЛАВА 5. Модульная арифметика: вычисление в классах вычетов 93 Д’ u2clintJ (tmpj, b); err = maddj (aj, tmpj, cj, mJ); return err; } В следующей главе мы пополним нашу коллекцию смешанных функций с аргументом типа USHORT еще двумя функциями. А за- канчивая эту главу, мы бы хотели, используя модульное вычитание, построить еще одну полезную вспомогательную функцию, которая определяла бы, являются ли две переменные типа CLINT представи- телями одного и того же класса вычетов по модулю т. В основу функции mequj() положено определение отношения сравнимости: а = b mod т <=> т | (а - Ь). Чтобы выяснить, сравнимы ли два CLINT-объекта по модулю mJ, нам нужно всего лишь применить функцию msubj(aj, bj, rj, mJ) и проверить, равен ли нулю полученный остаток rj. Функция: Проверка сравнимости по модулю т Синтаксис: int mequj (CLINT aj, CLINT bj, CLINT mJ); Вход: aj, bj (операнды), mJ (модуль) Возврат: 1, если (aJ == bj) по модулю mJ 0 в противном случае int mequj (CLINT aj, CLINT bj, CLINT mJ) { CLINT rj; if (EQZ_L (mJ)) { return E_CLINT_DBZ; } msubj (aj, bj, rj, mJ); return ((0 == DIGITS_L (rj))?1:0); ~x }
рЛАВА 6. Все дороги ведут к... модульному возведению в степень - Скажите, пожалуйста, куда мне отсюда идти? - А куда ты хочешь попасть? - ответил Кот. - Мне все равно... - сказала Алиса. - Тогда все равно, куда и идти, - заметил Кот. - ...только бы попасть куда-нибудь, - пояснила Алиса. - Куда-нибудь ты обязательно попадешь, - ска- зал Кот. - Нужно только достаточно долго идти. Льюис Кэррол, '! Приключения Алисы в Стране Чудес, (Перевод с английского Н. Демуровой) В дополнение к правилам вычисления суммы, разности и произве- дения в классах вычетов определим операцию возведения в сте- пень, где показатель указывает, сколько раз основание умножается само на себя. Как правило, возведение в степень реализуется рекур- сивным вызовом операции умножения: для а из кольца справед- ливо а := 1 и а := а • а . Легко видеть, что для операции возведения в степень в выпол- няются обычные правила (см. главу 1): ае • af= ae+f, а" be = (а b)e, (А'/ = а!. 6.1. Первые шаги Самый простой способ модульного возведения в степень - рекур- сивно применять указанное выше правило, умножая основание а само на себя е раз. Для этого требуется е - 1 модульных умноже- ний, а это для наших целей уж слишком много. Более эффективный способ иллюстрируется следующими примера- ми, в которых рассматривается двоичное представление показателя: & л15 23+22+2+1 = а [а2 а] а. а16 = а2 а2
96 Криптография на Си и C++ в действии Здесь для возведения основания в 15-ю степень требуется всего шесть умножений, тогда как в первом способе нам потребовалось бы 14 умножений. Половина из них - это возведение в квадрат, для которого, как мы знаем, нужно примерно вдвое меньше машинного времени по сравнению с обычным умножением. Для возведения й 16-ю степень требуется всего 4 возведения в квадрат. j Как мы увидим, алгоритмы вычисления экспоненты ае по модулу т, использующие двоичное представление показателя, как правило^ намного более предпочтительны, чем первый подход. Но прежде следует отметить, что промежуточные результаты многократного целочисленного умножения быстро занимают столько памяти, что их не в состоянии хранить ни один компьютер в мире, поскольку из р = аь следует log р = b log а, таким образом, число разрядов экспоненты аь есть произведение показателя на число разрядов чг основания. Однако эту проблему можно решить, если проводить вычисления в кольце классов вычетов Zzn посредством модульного умножения. Фактически в большинстве прикладных задач требуется возведение в степень именно по модулю т, так что эту ситуацию и будем рассматривать. Пусть е = (en_ien_2- • -ео)2, где e„_i > 0, - двоичное представление по- казателя е. Тогда следующий бинарный алгоритм требует Llog2eJ = п модульных возведений в квадрат и 8(e) - 1 модульных умножений, где 5(e);=Sei i=0 есть число единиц в двоичном представлении показателя е. Если считать, что каждый разряд принимает значение 0 или 1 равноверо- ятно, то можно сказать, что среднее значение 8(e) = п/2 и алгоритм требует всего |Qog2 умножений. Бинарный алгоритм вычисления ае по модулю т J 1. Вычислить р<г-ае,,~' и/«—л-2. 2. Положить р <— р mod т. 3. Если е, = 1, то положить р р • a mod т. 4. Положить I <г- i - 1; при i > 0 вернуться на шаг 2. ? 5. Результат: р. ! Следующая функция, реализующая этот алгоритм, дает хорошие результаты уже для малых показателей степени, представимых ти- пом USHORT.
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 97 _—- функция: Смешанное модульное возведение в степень с показателем типа USHORT Синтаксис: int umexpj (CLINT basj, USHORT e, CLINT pj, CLINT mJ); Вход: basj (основание) e (показатель) mJ (модуль) Выход: pj (вычет по модулю mJ) Возврат: E-CLINTJDK, если все в порядке E_CLINT_DBZ в случае деления на 0 int umexpj (CLINT basj, USHORT e, CLINT pj, CLINT mJ) { CLINT tmpj, tmpbasj; USHORT к = BASEDIV2; int err = E_CLINT_OK; if (EQZ_L (mJ)) { return E-CLINT-DBZ; /* Деление на нуль */ } if (EQONE_L (mJ)) { SETZERO.L (pj); /* Модуль = 1 ==> Остаток = 0 */ return EJDLINTOK; } if (e == 0) /* Показатель = 0 ==> Остаток = Г/ SETONE_L (pj); return E_CLINT_OK; } if (EQZ_L (basj)) { SETZEROJ_ (pj);
Криптография на Си и C++ в действии] return E_CLINT_OK; I } I modj (basj, mJ, tmpj); 1 cpyj (tmpbasj, tmpj); 1 После различных проверок определяем позицию старшего еди- ничного разряда показателя е. Переменная к используется в ка- честве маски отдельных двоичных разрядов показателя е. Затем к сдвигается еше на одну позицию вправо, что соответствует опе- рации i <— п - 2 на шаге 1 алгоритма. while ((е & к) == 0) { к »= 1; } к »= 1; Для остальных разрядов показателя е выполняем шаги 2 и 3. Маска к служит в качестве счетчика циклов и каждый раз сдвига- ется на один разряд вправо. Затем выполняется умножение на основание экспоненты по модулю mJ. while (k != 0) { msqrj (tmpj, tmpj, mJ); if (e & k) { mmulj (tmpj, tmpbasj, tmpj, mJ); } k »= 1; } cpyj (pj, tmpj); return err; Преимущества бинарного алгоритма возведения в степень особенно видны, если основание степени мало. Для основания, имеющего
j-дАВА 6. Все дороги ведут к... модульному возведению в степень 99 тип USHORT, все умножения р <— р • a mod т на шаге 3 бинарного алгоритма имеют тип CLINT * USHORT по модулю CLINT. Это дает существенное увеличение скорости по сравнению с другими алго- ритмами, которые в этом случае потребовали бы умножения двух переменных типа CLINT. Конечно, возведения в квадрат (шаг 2) ис- пользуют объекты типа CLINT, но здесь мы можем использовать более быструю функцию. Таким образом, попробуем реализовать функцию возведения в сте- пень wmexpJO, парную к функции umexpJO и применяемую для основания типа USHORT. Выделение по маске разрядов показателя степени - хорошее подготовительное упражнение с точки зрения последующих «больших» функций возведения в степень. По суще- ству, мы последовательно сравниваем все разряды показателя с переменной Ь, первоначально имеющей 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 (mJ)) { X return E_CLINT_DBZ; Г Деление на нуль */ }
100 Криптография на Си и C++ в действии if (EQONE.L (mJ)) { SETZERO J- (restj); return E_CLINT_OK; } if (EQZ_L (e J)) { SETONEJ. (restj); return E_CLINT_OK; } if (0 == bas) { SETZERO.L (restj); return E_CLINT_OK; /* Модуль = 1 ==> Остаток = 0 */ SETONEJ. (pj); cpyj (zj, ej); Разряды показателя zj обрабатываются, начиная co старшего ненулевого разряда в старшем слове показателя; при этом мы всегда выполняем сначала возведение в квадрат, а затем, если нужно, умножение. Проверка разрядов показателя осуществляется в выражении if ((w & b) > 0) путем их маскирования поразрядной операцией AND. Ь = 1 « ((IdJ (zj) - 1) & (BITPERDGT - 1UL)); w = zJ[DIGITS_L (zJ)]; for (; b > 0; b »= 1) { msqrj (pj, pj, mJ); if ((w & b) > 0)
101 ГЛАВА 6. Все дороги ведут к... модульному возведению в степень ummulj (pj, bas, pj, m_l); л lx: } } ) Затем обрабатываются оставшиеся разряды показателя. for (k = DIGITSJ- (zj) - 1; к > 0; к-) { w = zj[k]; for (b = BASEDIV2; b > 0; b »= 1) { msqrj (pj, pj, mJ); if ((w & b) > 0) { ummulj (pj, bas, pj, mJ); } } } cpyj (restj, pj); return EJDLINTJDK; } 6.2. М-арное возведение в степень Обобщив бинарный алгоритм со стр. 96, можно еще уменьшить число модульных умножений при возведении в степень. Суть под- хода состоит в том, чтобы записать показатель в системе счисления с основанием, большим 2, и заменить умножение на а на шаге 3 умножением на степени числа а. Итак, пусть показатель е представлен в системе счисления с осно- ванием М\ е - (еп^еп_2---ео)м, где число М мы определим позже. Для вычисления степеней ае mod т используется следующий алгоритм.
102 Криптография на Си и C++ в действии М-арный алгоритм вычисления ае по модулю т 1. Вычислить и запомнить таблицу значений a2 mod т, a3 mod т, .. a4'1 mod т. ' 2. Положить р <— ае,,~1 и / <— п - 2. 3. Положить р <г-рм mod т. 4. Если е[ Ф 0, то положить р <— pa ' modm . 5. Положить i «— i - 1; при i > 0 вернуться на шаг 3. 6. Результат: р. Понятно, что число умножений зависит от числа разрядов показа- (6.1) (6.2) теля е и, следоватег Поэтому определил бы как можно боль ре выше для 216. Т численные на шаге памяти на хранение Исходя из первого М= 2к. Согласно в рассматриваем как Потребуем, чтобы ь |_logM еJlog2 м = |_1< возведений в квадр; LlogM еJpr<>, * °) = [ЬНО, от 4 ЧИСЛО ше возв огда чи 1, буд таблиц услови порому функци ia шаге 3g2<d it, а на log2e Л выбора основания системы счисления М. М так, чтобы на шаге 3 использовалось рдений в квадрат, как это было в приме- :сло умножений на степени числа а, вы- ет минимальным, что оправдает затраты ы. я, выбираем М равным степени двойки: условию, число модульных умножений ю от М: 3 выполнялось j шаге 4 в среднем ; РГ(6; Ф 0) j. 1- модульных умножений, где ; рГц^о)=1-Е <! м есть вероятность того, что разряд с{ показателя е ненулевой. Учи- тывая, что для построения таблицы предвычислений нужно М - 2 (6.3) умножений, получаем, чт ц,(£):= 2к - 2 + [log2 еJ+ о 1 М-арный алгоритм требует в среднем Og2gf, И к Д 2к ) (6.4) — 2^ — 2 + [_log2 ^J+ модульных возведи f 2к -Р 1 к2к J ний в квадр ат и умножений.
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 103 j В таблице 6.1 приведены значения числа модульных умножений для вычисления ae mod m, когда показатель е и модуль m имеют длину 512 бит и М = 2к. Там же даны затраты памяти на хранение таблицы предвычислений степеней a mod ш, обусловленные вычислением произведения (2к - 2) * CLINTMAXSHORT * sizeof(USHORT). Таблииа 6.1. Требования при возведен и и в степень к Число умножений Память (в байтах) 1 766 0 2 704 1028 3 666 3084 4 644 7196 5 640 15420 6 656 31868 in • ; - Как видно из таблицы, среднее число умножений достигает ми- нимального значения 640 при к = 5, тогда как объем требуемой памяти увеличивается при каждом следующем к примерно вдвое. А как же будут меняться временные затраты для больших показа- телей степени? На этот вопрос отвечает таблица 6.2, в которой приведено число модульных умножений, выполняемых при возведении в степень, при различных длинах показателя и различных М = 2к. В таблицу, помимо степеней двойки, включено значение 768, поскольку ключ такой длины часто используется в криптосистеме RSA (см. главу 16). Наименьшие значения для числа умножений выделены жирным шрифтом. При рассмотрении диапазонов чисел, для обработки которых был разработан пакет FL1NT/C, оказывается, что при к = 5 мы получаем универсальное основание М = 2к системы счисления. Но в этом случае нам потребуется довольно много памяти (15 кбайт) для хра- нения таблицы предвычислений и2, я3, ..., и31. Согласно работе [Cohe], п. 1.2, М-арный алгоритм можно улучшить, выполняя на этапе предвычислений не М - 2, а только М/2 умножений, то есть в два раза сократить затраты памяти. И теперь наша задача - вычис- лить ае mod m, где е = (^_1^-2«--<?о)л/ - представление показателя в системе счисления с основанием М = 2к.
104 Криптография на Си и C++ в действии Таблииа 6.2. Число двоичных разрядов показателя Число умножений к 32 64 128 512 768 1024 2048 4096 1 45 93 190 766 1150 1534 3070 6142 для типичных длин показателя 2 44 88 176 704 1056 1408 2816 5632 и различных оснований 2к 3 46 87 170 666 996 1327 2650 5295 4 52 91 170 644 960 1276 2540 5068 5 67 105 181 640 945 1251 2473 4918 6 98 135 209 656 954 1252 2444 4828 7 161 197 271 709 1001 1294 2463 4801 8 288 324 396 828 1116 1404 2555 4858 Л/-арный алгоритм возведения в степень с сокращенной таблицей предвычислений Вычислить и запомнить таблицу 7 2к -1 a mod т, ..., a mod т. значений a3 mod in, a5 mod т 2. Если еп^ = 0, то положить р <— 1. Если еп^ Ф 0, то представить еп_\ в виде еп.\ = 2гм, где и ное. Вычислить р <— ali mod т. нечет В обоих случаях положить i <— п - 2. 3. Если О, то положить Р rnodm вычислив f-ИГ.)1 mod т (k-кратное возведение в квадрат по мо Дулю т). Если е> * 0, то представить в виде et = 2'п, где и положить р <— р mod/н, затем р <— pa mod т - нечетное и, наконец р р2 rnodm . 4. Положить i <— i - 1; при i > 0 вернуться на шаг 3. 5. Результат: р. Весь секрет этого алгоритма состоит в разделении операций возве дения в квадрат на шаге 3 таким образом, что возведение а в сте пень регулируется четным делителем 2' числа в/. Вместе с возведе ниями в квадрат остается и возведение числа а в нечетную степень Баланс между операциями умножения и возведения в степень сме щается в сторону более предпочтительного возведения в степень и причем вычислять и хранить нужно лишь степени числа а с нечет ным показателем.
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 105 Теперь нам нужно однозначно представить разряд показателя в виде ei = 21и, где и - нечетное. Чтобы всегда иметь под рукой числа tn и, можно составить таблицу (см., например, таблицу 6.3 для к = 5). Таблииа 6.3. ej t и е; t u ej t u Значения 0 0 11 0 11 22 1 11 параметров в разложении 1 0 1 12 2 3 23 0 23 разрядов 2 1 1 13 0 13 24 3 3 показателя в произведение 0 3 14 1 7 25 0 25 степени двойки 4 2 1 15 0 15 26 1 13 и нечетного 0 5 16 4 1 27 0 27 числа 1 3 17 0 17 28 2 7 7 0 7 18 1 9 29 0 29 . ; 8 3 1 19 0 19 30 1 15 :я> 9 0 9 20 2 5 30 0 31 г; 10 1 5 21 0 21 Для вычисления этих значений воспользуемся вспомогательной функцией twofactJO, которая будет введена в п. 10.4.1. И прежде чем запрограммировать Л/-арный алгоритм, нам осталось решить всего одну проблему: как, исходя из двоичного представления по- казателя или представления в системе счисления с основанием В = 216, быстро перейти к представлению с основанием М = 2к для произвольного к > 0? В этом нам поможет небольшой «трюк» с ин- дексами, позволяющий получить требуемые разряды et представле- ния в системе счисления с основанием М из представления е в сис- теме счисления с основанием В. Итак, пусть (ег_1Ег-2---£о)2 - пред- ставление показателя е в системе счисления с основанием 2 (оно потребуется нам для определения числа г двоичных разрядов). Пусть (eH_i^M_2...e0)fi - представление показателя е как числа типа CLINT в системе счисления с основанием В = 216 и (е'п-1е'п^“е'о)м - представление показателя е в системе счисления с основанием М = 2 , к < 16 (М не должно превышать основания В). Представление пока- зателя е в памяти как CLINT-объекта е_1 задается последовательно- стью значений ej[i] типа USHORT для 1=0, ..., и + 1: [и + 1], [е0], [ej, ... , [ем_1], [0]. Заметим, что здесь мы добавили ведущий нуль. Пусть f := |_-~J и пусть sf := [_^-J и := ki mod 16 для i = 0, Справедливы следующие утверждения: 1. Число разрядов в представлении (e'n_ie'n_2• • •е'о)м равно/+ 1, то есть /г - 1 =/. Л еК w _ , 2. Разряд ' содержит младший бит разряда е j.
106 Криптография на Си и C++ в действии 3. Значение di указывает позицию младшего бита разряда е\ в е (отсчет позиций начинается с нуля). Если i </и dt > 16 - к, то не все биты разряда е\ входят в ех ; оставшиеся (старшие) биты разряда е t входят в es,.+1. Таким образом, интересующий нас разряд соответ- ствует к младшим двоичным разрядам числа Таким образом, для вычисления разряда е h i Е {0, ...,/} получаем । следующее выражение: И............. (^5) e'i = ((e_l[s, + 1] | (e_l[s, + 2] « BITPERDGT)) »</,) & (2* - 1);. Если для простоты положить ej[sy + 2] <— 0, то это выражение v будет справедливо и для i =f 1 5 Таким образом, мы нашли эффективный способ доступа к разрядам i ; показателя в его CLINT-представлении (это стало возможным бла- годаря тому, что в нем они представлены в системе счисления с ос- нованием 2\ к < 16), сэкономив явные преобразования показателя. Теперь число умножений и возведений в квадрат равно . (6.6) , , । / 2*-1^ S ц2(&) := 2*~ +|_log2ej 1 + —- , ! и по сравнению с Ц1(&) (см. стр. 102) затраты на предвычисления сократились вдвое. Теперь таблица, задающая наиболее подходя- щие значения к (таблица 6.4), несколько изменилась. Таблииа 6.4. Число двоичных разрядов показателя Число умножений к 32 64 128 512 768 1024 2048 4096 1 47 95 191 767 1151 1535 3071 6143 лля типичных ллин показателя 2 44 88 176 704 1056 1408 2816 5632 и различных оснований 2к 3 44 85 168 664 994 1325 2648 5293 4 46 85 164 638 954 1270 2534 5062 5 53 91 167 626 931 1237 2459 4904 6 68 105 279 626 924 1222 2414 4798 7 99 135 209 647 939 1232 2401 4739 8 162 198 270 702 990 1278 2429 4732^ Начиная с показателя длины 768, наиболее подходящие значения к стали на 1 больше, чем в предыдущей версии алгоритма возведения в степень (см. таблицу 6.2), тогда как число необходимых модульных
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень умножений заметно сократилось. Вероятно, эта процедура в целом более предпочтительна, чем предыдущий вариант. Теперь цичто не мешает нам реализовать алгоритм. - Продемонстрируем реализацию рассмотренных принццпов на примере адаптивной процедуры, использующей соответс‘гВуЮ1дее оптимальное значение к. Для этого вновь сошлемся на Ко^а [Cohe] и вслед за ним найдем наименьшее целое к, удовлетворяющее неравенству (6.7) Д log2 е < к(к + 1)22к 2м-к-2 которое выводится из приведенной выше формулы ц2(£) Для числа необходимых умножений и условия |i2(^+l) - \^i(k) > 0. Константа Llog2<?J, определяющая число модульных возведений в кв^драт во всех рассмотренных выше алгоритмах, сократилась; остались ТОлько «настоящие» модульные умножения, то есть те, где сомножители различны. При реализации процедуры возведения в степень с перченным значением к требуется большой объем оперативной пад1ЯТИ для хранения таблицы предвычислений (степеней числа я); Для £ = 8 необходимо около 64 кбайт для 127 переменных типа CLjnt (это получается в результате умножения (27 - 1) * sizeof(USHoRT) * CLINTMAXSHORT), при этом два автоматически возникаю1цих поля CLINT не учитываются. Для приложений, использующих процесс0рЫ или модели памяти с сегментированной 16-разрядной архитекту- рой, это уже максимально допустимый предел (по этому поводу см., например, [Dune], глава 12, или [Petz], глава 7). В В зависимости от используемой платформы осуществлять доступ к памяти можно по-разному. Память, необходимая для функции mexp5J(), берется из стека (как и для всех переменных тищ CLINT), тогда как под каждый вызов функции mexpkJO выделяет^ дина- мическая память. Дабы избежать сопутствующего этому увели- чения затрат, можно зарезервировать максимально необ^0ДИМуЮ память при однократной инициализации и освободить ее тоЛЬКо по окончании всей программы. В любом случае можно подчинить рас- пределение памяти конкретным требованиям и обращал на них внимание в комментариях к соответствующему коду. Еще одно замечание по реализации: всегда рекомендуется прове- рять, достаточно ли для данного приложения основания _ 25. Экономия времени при увеличении к оказывается не так у^ велика по сравнению с общим временем вычислений и с тем, чтоб^ оправ- дать большие расходы памяти и, соответственно, затраты ее рас_ пределение. Типичные оценки времени, затрачиваемого разяичными алгоритмами возведения в степень, приведены в Прило^ении d
108 Криптография на Си и C++ в действии Исходя из этих оценок, можно решать, каким из указанных алго- ритмов пользоваться. Для М = 25 алгоритм реализован в виде функции mexp5J() в пакете FLINT/C. Макрос EXP__L() позволяет установить используемую функцию возведения в степень: mexp5_l() или mexpk_l() с перемен- ным значением к. Функция: Модульное возведение в степень Синтаксис: int mexpkj (CLINT basj, CLINT expj, CLINT pj, CLINT mJ); Вход: basj (основание) expj (показатель) mJ (модуль) Выход: p_l (вычет по модулю mJ) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 E_CLINT_MAL, если функция malloc() выдала ошибку Начинаем с построения таблицы, в которой записаны значения е; = 2fu, где и - нечетное, 0 < е, < 28. Таблииа представляется в виде двух векторов. Первый вектор twotab[] содержит показатели t числа 2\ элементами второго - oddtabf] - являются нечетные множители и разряда 0 < е^ < 25. Целиком таблица, разумеется, хранится в исходном коде FLINT/C. static int twotab[] = {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 USHORT oddtab[] = {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) { В описаниях зарезервирована память под показатели степени плюс ведущий нуль. Кроме того, необходимо будет еше выделить указатель clint **aptrj, который будет содержать указатели на вычисляемые степени переменной basj. В асс_1 будут храниться промежуточные результаты.
109 ГЛАВА 6. Все дороги ведут к... модульному возведению в степень CLINT aj, a2J; clint eJ[CLINTMAXSHORT + 1]; CLINTD accj; clint **aptrj, *ptrj; 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 J_ (pj); return EJDLINT-OK; } /* Модуль = 1 ==> Остаток = 0 */ Копируем основание и показатель в рабочие переменные aj и ej и убираем все ведущие нули. cpyj (aj, basj); cpyj (ej, expj); Теперь обрабатываем тривиальные случаи а0 = 1 и 0е = 0 (е > 0). if (EQZ_L (ej)) { SETONE_L (pj);
110 Криптография на Си и C++ в действии return E_CLINT_OK; } if (EQZ_L (aJ)) { SETZERO J_ (pj); return E_CLINT_OK; } Далее определяем оптимальное значение к; значения 2к и 2к-1 хранятся в pow2k и в k_mask соответственно. Для этого исполь- зуем функцию которая возврашает число двоичных разря- дов аргумента. Ige = Id J (e_l); k = 8; while (k> 1 && ((k- 1) * (k « ((k-1)« 1))/((1 « k) - k- 1)) >= Ige - 1) { -k; } pow2k = 1U « k; k_mask = pow2k - 1U; Выделяем память под указатели на степени величины aj. Осно- вание aj приводится по модулю mJ. if ((aptrj = (clint **) malloc (sizeof(clint *) * pow2k)) == NULL) { return E_CLINT_MAL; modj (aj, mJ, aj); aptrj[1] = a J;
. Все дороги ведут к... модульному возведению в степень 111 При к > 1 выделяем память под таблицу предвычислений. При к = 1 этого делать не нужно, поскольку тогда никаких предвычислений не требуется. В приведенных ниже присваиваниях указателю aptrj[i] следует помнить, что при сложении смешения с указате- лем компилятор сам правильно масштабирует результат, так как он оперирует с объектами типа «указатель на р». Как уже отмечалось, оперативную память можно выделять и при однократной инициализации. В этом случае указатели на CLINT- объекты должны содержаться в глобальных переменных вне функции или в переменных класса static в функции mexpkJO. if (k> 1) { if ((ptrJ = (clint *) malloc (sizeof(CLINT) * ((pow2k »1)-1))) == NULL) { return E_CLINT_MAL; } aptrj[2] = a2J; for (aptrj[3] = ptrj, i = 5; i < (int)pow2k; i+=2) { aptrJ[i] = aptrJ[i - 2] + CLINTMAXSHORT; } Теперь выполняем предвычисления степеней переменной а, хра- нимой в aj. Вычисляются значения а3, а5, а7, ..., а1 1 (а2 играет лишь вспомогательную роль). msqrj (aj, aptrj[2], mJ); for (i = 3; i < (int)pow2k; i += 2) { mmulj (aptrj[2], aptrj[i - 2], aptrJ[i], mJ);
112 Криптография на Си и C++ в действии На этом кончаются отличия случая к > 1. К показателю добавля- ется нуль в старшем разряде. й (MSDPTFLL (е_1) + 1) = 0; Определяем значение f (представленное переменной noofdigits). noofdigits = (Ige - 1 )/k; fk = noofdigits * k; Для разряда ej определяем слово Sj (переменная word) и бит dj (переменная bit). word = fk » LDBITPERDGT; /* fk div 16 7 bit = fk & (BITPERDGT-1U); /* fk mod 16 7 Вычисляем разряд е^ по приведенной выше формуле; еп_! пред- ставлен переменной digit. switch (к) т* case 1: case 2: case 4: case 8: digit = ((ULONG)(eJ[word + 1]) » bit) & k_mask; break; default: digit = ((ULONG)(eJ[word + 1] | ((ULONG)eJfword + 2] « BITPERDGT)) » bit) & k_mask;
f-ддВА 6. Все дороги ведут к... модульному возведению в степень 113 В первый раз проходим шаг 3 алгоритма в случае digit = e^, 0. if (digit != 0) /* k-digit >07 { cpyj (accj, aptrj[oddtab[digit]]); Вычисляем p2 •, значение t устанавливаем по таблице twotabfenJ - число 2 в максимальной степени, деляшей е^; число р представлено переменной accj. t = twotabfdigit]; for (; t > 0; t-) { msqrj (accj, accj, mJ); } else /* k-й разряд == 0 7 { SETONE_L (accj); } Цикл no noofdigits, начиная c f - 1. for (-noofdigits, fk -= k; noofdigits >= 0; noofdigits-, fk -= k) Для разряда ej определяем слово s, (переменная word) и бит dj (переменная bit). word = fk » LDBITPERDGT; bit = fk & (BITPERDGT - 1U); /* fk div 16 7 /*fk mod 16 7
114 Криптография на Си и C++ в действии Вычисляем разряд е; по приведенной выше формуле; е, представ* лен переменной digit. switch (к) case 1: case 2: case 4: case 8: digit = ((ULONG)(eJ[word + 1]) » bit) & k_mask; break; default: digit = ((ULONG)(eJ[word + 1] | ((ULONG)eJ[word + 2] « BITPERDGT)) » bit) & k.mask; } Проходим шаг 3 алгоритма для случая digit = е, 0; значение t устанавливаем по таблице twotab[ej. if (digit != 0) /* к-digit >07 { t = twotab[digit]; 2^”^ и Вычисляем p а в accj. Для вычисления au определяем не- четный делитель и разряда е; по таблице aptr j [oddtab[ej]]. for (s = к -1; s > 0; s-) msqrj (accj, accj, mJ); mmulj (accj, aptrj[oddtab[digit]], accj, mJ);
рддВА 6. Все дороги ведут к... модульному возведению в степень 115 Вычисляем р2 ; значение р по-прежнему представлено перемен- ной accj. for (; t > 0; t--) { msqrj (acc J, асе J, mJ); } } else /* k-digit == 0 */ { 2^ Шаг 3 алгоритма для случая ej = 0: вычисляем р . for (s = k; s > 0; s-) { msqrj (accJ, accJ, mJ); } } } Цикл заканчивается; результат accj является степенью по моду- лю mJ. cpyj (pj, accj); И наконец, освобождается выделенная память. free (aptrj); if (ptrj != NULL) free (ptrj); return E_CLINT_OK;
116 Криптография на Си и C++ в действии Поясним алгоритм М-арного возведения в степень на численном примере. Для этого вычислим 1234667 mod 18577 с помощью функ- ции mexpkj(). 1. Предвычисления Представим показатель е = 667 в системе счисления с основанием 2кс к = 2 (см. Л7-арный алгоритм возведения в степень на стр. 102), получим е = (1010 011011)2?. Значение a mod 18577 равно 17354. Больше никаких степеней чис- ла а вычислять не требуется: 2к- 1 = 3. 2. Основной цикл Разряд показателя е-,= 2*и 21-1 2'-1 2°-1 2’-1 2°-3 р <— р2 mod п - 14132 13261 17616 13599 22 р +- р mod п - - 4239 - 17343 р <— p-au mod п 1234 13662 10789 3054 4445 р <— р2 mod п 18019 7125 - 1262 1 3. Результат I р = 1234667 mod 18577 = 4445. Рассмотрим частный случай возведения в степень, когда показатель является степенью двойки: 2к. Как мы видели ранее, это легко мож- но сделать путем ^-кратного возведения в квадрат. Показателю к в 2к будет соответствовать переменная к. Функция: Модульное возведение в степень в случае, когда показатель явля- ется степени двойки Синтаксис: int mexp2J (CLINT aj, USHORT k, CLINT pj, CLINT mJ); Вход: aj (основание) k (показатель к в 2k) mJ (модуль) Выход: pj (вычет aj2k по модулю mJ) Возврат: E_CLINTJ3K, если все в порядке E_CLINT_DBZ в случае деления на 0
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 117 int mexp2J (CLINT aj, USHORT k, CLINT pj, CLINT mJ) J { CLINT tmpj; if (EQZ_L (mJ)) { Y‘( return E_CLINT_DBZ; } При k > 0 возводим k раз a l в квадрат по модулю mJ. if (k > 0) { cpyj (tmpj, aj); while (k- > 0) { msqrj (tmpj, tmpj, mJ); } cpyj (pj, tmpj); } else В противном случае, при k = 0, нужно выполнить лишь приведе- ние по модулю mJ. { modj (aj, mJ, pj); } return E_CLINT_OK; }
118 Криптография на Си и C++ в действии 6.3. Аддитивные цепочки и окна На сегодняшний день опубликовано множество алгоритмов возве- дения в степень: одни удобны лишь для частных случаев, другие универсальны. Но цель всегда одна и та же - по возможности со- кратить число умножений и делений, как это было при переходе от бинарного к Л/-арному алгоритму. Алгоритмы бинарного и Л/-арного возведения в степень, в свою оче- редь, являются частными случаями аддитивных цепочек (см. [Knut], п. 4.6.3). Мы уже знаем, что при возведении в степень можно пред- ставить показатель в виде суммы: е = k + 1 => ае = ам = ака1. Пред- ПК** ' ставляя показатель в двоичной системе счисления: , ^к-2 . . е — ек_\ • 2 + ек_2 • 2 + ... 4- во, можно выполнить возведение в степень с помощью возведений в л квадрат и умножений (см. стр. 96): ' (( \2 Л У аеты\п= ... У1 ... ae°modn. Элементами соответствующей аддитивной цепочки являются пока- затели при степенях числа а: ^к-Ь ек-\' Ъ вк-\' 2 + в^_2, (в^_г 2 + ек^' 2, (в^_г 2 + в^-2)* 2 + e^-з, , ((Q-r 2 + в£_2)’ 2 + в£_3)- 2, (•••(Q-Г 2 + в^)’ 2 + ••• + в])- 2 + в0. Если для некоторого значения j показатель в7 = 0, то соответствую- щие элементы последовательности опускаются. Например, для числа 123 результатом бинарного метода будет аддитивная цепочка из 12 элементов: 1, 2, 3, 6, 7, 14, 15, 30, 60, 61, 122, 123. В общем случае последовательность чисел 1 = aQ, ах, а2, ...,аг= е,ъ которой для каждого i = 1, ..., г существует пара чисел (/, к) таких, что j < к < i и at = aj + ак, называется аддитивной цепочкой длины г для числа е. М-арный метод обобщает представление показателя на случай про- извольного основания. Цель у обоих методов общая - получить наи- более короткие аддитивные цепочки и тем самым снизить вычисли-
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 119 тельные затраты на возведение в степень. Для числа 123 23-арный метод дает аддитивную цепочку 1, 2, 3, 4, 7, 8 1 5, 30, 60, 120, 123- 24-арный метод - 1, 2, 3, 4, 7, 11, Д 28,’ 56, 112, 123. Эти две цепоч- ки значительно короче, чем цепочка в бинарном методе; для боль- ших чисел разница будет еще более значительной. Однако, раз уж мы говорим^ о временных затратах, следует отметить, что база данных а . а . а ..... а “, которую мы строим при инициализации Л/-арных методов, включает в себя и те степени ci. которые не нуж- ны для представления е по основанию М или для построения адди- тивной цепочки. Наихудшим случаем построения аддитивной цепочки является би- нарное возведение в степень: здесь цепочка имеет максимально 1 t возможную длину log2e + Н(е) - 15 где через //(<?) обозначен хем- мингов вес числа е. Снизу длина аддитивной цепочки ограничена числом log2e + log2//(e) - 2,13, более коротких цепочек быть не должно (см. [Scho] или [Knut], п. 4.6.3, упражнения 28, 29). В на- ; шем случае это означает, что длина самой короткой аддитивной . цепочки для е - 123 не может быть меньше 8, а значит, приведен- и ные выше результаты Л/-арных методов далеко не самые лучшие. До сих пор не существует полиномиального алгоритма, решающего задачу поиска кратчайшей аддитивной цепочки. Эта задача принад- лежит классу сложности NP, то есть относится К задачам, которые могут быть решены за полиномиальное время недетерминирован- ными методами. Иначе говоря, решение этих задач за полиноми- альное время можно лишь «угадать», в отличие от задач класса Р, w ; которые будут решены детерминированно. Неудивительно, что Р является подмножеством NP, поскольку все задачи, решаемые за полиномиальное время детерминированными методами, могут быть решены за то же время недетерминированными методами. Определение кратчайшей аддитивной цепочки является NP-полной задачей, то есть задачей, сложность которой не хменьше, чем слож- ность любой другой задачи из класса NP (см. [HKW], стр. 302). NP- полные задачи тем более интересны, что если хотя бы одна из них будет решена детерминированными методами за полиномиальное время, то и все остальные задачи из NP будут решены за полино- миальное время. В этом случае можно будет сказать, что классы Р и NP - это одно и то же множество задач. Задача о том, совпадают ли множества Р и NP, является основной нерешенной задачей тео- рии сложности. Однако в настоящее время превалирует мнение о том, что Р NP. Если число п в двоичной системе счисления имеет вид ц = (пк_хпк-2-• -п0)^ то #00 = (см. [HeQu], Глава 8).
120 Криптография на Си и C++ в действии Теперь ясно, что при практической реализации процедуры генерации аддитивных цепочек мы должны опираться на некие эвристики, то есть те приближенные математические методы, которые в данном случае работают эффективнее других: как, например, в случае с определением показателя к для 2^-арного возведения в степень. Например, в 1990 г. Я. Якоби (Y. Yacobi) [Yaco] установил связь между построением аддитивных цепочек и сжатием данных по методу Лемпеля-Зива (Lempel-Ziv); в его работе приведен также алгоритм возведения в степень, основанный на таком сжатии и М-арном методе. Для поиска кратчайшей аддитивной цепочки возможно дальнейшее обобщение М-арного метода возведения в степень, чем мы сейчас и займемся. В методах окна, в отличие от М-арного метода, показа- тель представляется не разрядами в системе счисления по фиксиро- ванному основанию М, а разрядами переменной двоичной длины. Так, например, разрядом показателя может быть длинная последо- вательность двоичных нулей, называемая нулевым окном. Вспом- ним М-арный алгоритм со стр. 104: ясно, что для нулевого окна длины I потребуется /-кратное возведение в степень, то есть третий шаг алгоритма примет вид: 3. 2^ Положить р р modw = (/ раз) mod т. Ненулевые разряды обрабатываются либо как окна фиксированной длины, либо как изменяемые окна максимальной длины. Как и в М-арном случае, для любого ненулевого окна (называемого далее, не совсем удачно, «1-окном») длины t помимо повторного возведения в квадрат выполняется еще дополнительное умножение на некото- рый элемент таблицы предвычислений: 3'. Положить р <— р2 mod т, а затем р «— paei modm. Число элементов таблицы предвычислений зависит от допустимой максимальной длины 1-окна. Отметим, что младший разряд 1-окна всегда равен 1, то есть 1-окно всегда нечетно. Таким образом, нам не надо здесь раскладывать разряд показателя, как на стр. 104, на четный и нечетный множители. С другой стороны, при возведении в степень мы двигаемся слева направо, а это значит, что, прежде чем возводить в степень, нам потребуется полностью разложить показатель на множители и, кроме того, помнить это разложение. Тем не менее, если мы начнем раскладывать показатель со старшего разряда и будем двигаться слева направо, то можно обрабатывать каждое 0- или 1-окно сразу же по мере заполнения. Отсюда, оче- видно, следует, что у нас получатся и четные 1-окна, но этот случай алгоритмом допускается.
в. Все дороги ведут к... модульному возведению в степень 121 По существу, разложение показателя на 1-окна и в том, и в другом направлении выполняется одним и тем же алгоритмом. Сформули- руем его для разложения справа налево. Разложение целого числа е на 0- и 1-окна t фиксированной длины 1 1. Если младший двоичный разряд числа е равен 0, то начать 0-окно и перейти на шаг 2; в противном случае начать 1-окно и перейти на шаг 3. 2. Пока не появится 1, добавлять следующие по старшинству дво- ичные разряды в 0-окно. Как только появится 1, закрыть 0-окно, начать 1-окно и перейти на шаг 3. 3. Собрать следующие I - 1 двоичных разрядов в 1-окне. Если по- следующий разряд равен 0, то начать 0-окно и перейти на шаг 2; в противном случае начать 1-окно и перейти на шаг 3. Алгоритм заканчивает работу после того, как пройдены все разряды числа е. При разложении слева направо начинаем со старшего двоичного разряда и действуем по аналогии. Если предположить, что в числе е нет начальных нулей, то алгоритм не может закончиться на шаге 2, а только на шаге 3. Приведем два примера. ✓ Пусть е = 1896837 = (111001111000110000101 )2 и I = 3. Раскладыва- ем е, начиная с младшего двоичного разряда: е = 111 001 111 00011 0000 101. м При I = 4 получаем разложение ’ •ta е = 111 00 1111 00011 0000101. Рассмотренный выше 2*-арный метод при к = 2 дает разложение е = 01 И 00 11 11 00 01 10 00 01 01. Таким образом, при I = 3 получаем в разложении числа е пять 1-окон, а при I = 4 только четыре; в обоих случаях требуется одно и то же число дополнительных умножений. Разложение по 22-арному методу содержит восемь 1-окон, требует в два раза больше дополнитель- ных умножений, чем в случае I = 4 и, значит, вряд ли заслуживает внимания. ✓ При выполнении той же процедуры слева направо, начиная со старших разрядов, при I = 4 и е = 123 получаем разложение е = 1110 0 1111 000 ПОР 00 101, также с четырьмя 1-окнами, среди которых, как уже отмечалось, есть четные. Теперь мы, наконец, можем сформулировать алгоритм разложения показателя методом окон с учетом обоих направлений разложения.
122 Криптография на Си и C++ в действ| Алгоритм вычисления вычета ае mod т с разложением показателя е на нечетные 1-окна (максимальной) длины I 1. Разложить показатель е на 0- и 1-окна длины соответственно. 2. Вычислить и запомнить я3 mod m, я5 mod /и, я7 mod ш, я2* mod т. 3. Положить Р mod т и i <— к - 2. 4. Положить Р р'1 mod т. 5. Если со, * 0, то положить Р Ра^1 mod т. 6. Положить i <— i - 1; при i > 0 вернуться на шаг 4. 7. Результат: р. Если среди 1-окон есть четные, то вместо шагов 3-7 выполняются следующие: J 3'. Если avi = 0, то положить а (ik-i Если Щи Ф 0, то представить ccvi в виде = 2 я, где и - нечет- ное; положить р <— аи mod т, а затем р <— р2' mod т . В обоих случаях положить i <— к - 2. 4'. Если СО, = 0, то положить Если со, Ф 0, то представить сог в виде со, = 2'я, где и - нечетное; положить р <— р2' гаъ&т, затем р <— раи mod т и, наконец» р <— р2 mod т . 5'. Положить i <— i - 1; при i > 0 вернуться на шаг 4Z. 6'. Результат: р.
6. Все дороги ведут к... модульному возведению в степень 123 6.4- Приведение по модулю и возведение в степень методом Монтгомери Теперь оставим аддитивные цепочки и обратимся к другому подхо- ду, интересному, прежде всего, с точки зрения алгебры. Этот под- ход позволяет заменить умножение по модулю нечетного числа п умножением по модулю 2*, которое не требует деления в обычном понимании и, следовательно, является более эффективным, чем с приведение по модулю произвольного числа п. Этот замечательный метод был опубликован в 1985 г. П. Монтгомери (Р. Montgomery) [Mont] и с тех пор широко применяется. В основе метода лежит следующее свойство. Пусть пит- взаимно простые целые числа; г"1 мультипликативно обратно к г по модулю и; и-1 мультипликативно обратно к п по мо- дулю г. Пусть п := -/Г1 mod г и т := tn mod г. Для любого целого t справедливо сравнение (6.8) t + mn ч 1 ' ------= tr mod п . г Заметим, что при вычислении левой части сравнения мы оперируем со сравнениями по модулю г (поскольку t + тп = 0 mod г, остаток “° от деления на г равен нулю), но не по модулю п. Если выбрать в * ' качестве г степень двойки 25, то 5 младших битов числа х и будут задавать остаток от деления х на г, а деление х на г выполняется простым сдвигом числа х на 5 бит вправо. Таким образом, вся пре- '• лесть сравнения (6.8) состоит в том, что его левая часть вычисляет- ся значительно быстрее, чем правая. Здесь нужны всего две опера- ции, для выполнения которых можно воспользоваться функциями mod2J() (см. п. 4.3) и shiftJO (см. п. 7.1). Такая процедура вычисления вычета по модулю п называется преобразованием Монтгомери. Поскольку здесь требуется, чтобы числа п и г были взаимно простыми, число п должно быть нечетным. Ниже мы покажем, что с помощью преобразования Монтгомери можно выполнять модульное возведение в степень значительно быстрее, чем предыдущими методами. А пока уточним некоторые моменты. Корректность сравнения (6.8) можно проверить довольно просто. Подставим в левую его часть вместо т значение tn mod г (см. фор- мулу (6.9)), затем заменим tn mod г на tn - r[_tn7 г]е7 (получим (6.10)), наконец, выразим п в виде целого числа (г г - 1)/ п для некоторого г G 77 и получим (6.11). Результатом приведения по модулю п будет формула (6.12): (6.9) t + тп = t + n(tn mod г) г г
124 Криптография на Си и C++ в действи (6.10) t + ntn tn ж = п .ц* г |_ г J И. (6.11) _t + t(rr' -1) (6.12) = tr~x mod и. W Пусть n, t, re НОД(и, r) = 1 и n := -n~x mod г. Тогда из формулы (6.8) следует, что для (6.13) ДО := t + (tn mod г) п справедливо (6.14) ДО = t mod п, яН (6.15) ДО = 0 mod г. W К этому результату мы еще вернемся. Чтобы применять преобразование Монтгомери, будем проводить вычисления по модулю п в полной системе вычетов (см. главу 5): R := R(r, п) := {ir mod п | 0 < i < п} с подходящим г := 2s > 0, таким, что 25-1 < п < 2Л. Определим произ- ведение Монтгомери «х» чисел а и b из R как а х b := abr~x mod п, где через г"1 обозначено число, мультипликативно обратное к г по модулю п. Имеем а х b = (ir)(jr)r~x = (ij)r mod п е R, то есть произведение х двух элементов множества R тоже принад- лежит множеству R. Произведение Монтгомери вычисляется с помощью преобразования Монтгомери. Поскольку числа п и г вза- имно просты, расширенным алгоритмом Евклида (см. п. 10.2) по- лучаем линейное представление их наибольшего общего делителя: 1 = НОД(и, г) = г'г-пп, где п := -/Г1 mod г. Тогда из линейного представления : 1 = г г mod п и 1 == -ни mod г,
ведут к... модульному возведению в степень 125 то есть число г = г-1 mod п мультипликативно обратно к г по моду- лю п, а число п = -n~l mod г, взятое со знаком «-», мультиплика- тивно обратно к п по модулю г (здесь мы немного забежали вперед; см. п. 10.2). Произведение Монтгомери вычисляется по следующему алгоритму. Вычисление произведения Монтгомери а X Ъ в R(r, п) 1. Положить t <— ab. 2. Вычислить т <— tn'mod г. 3. Вычислить и <— (/ + тп) / г (деление выполняется нацело; см. выше). 4. Если и > п, то результат: и - п; иначе результат: и. Параметры алгоритма выбраны так, что а, b < п и т, п < г, тогда и < 2п (см. формулу (6.21)). Этот алгоритм требует вычисления трех произведений больших чисел: одно на шаге 1 и два на этапе приведения (шаги 2, 3). Поясним алго- ритм на маленьких числах. Пусть а = 386, b = 257, п = 533. выберем г = 210. Тогда п = -n~l mod г = 707, т = 6,t + тп = 102400 и и = 100. Теперь можем вычислять произведение ab mod п, где число п не- четное, следующим образом. Сопоставим числам а и b элементы множества R: а <— ar mod п, b' <— br mod /1, затем найдем произве- дение Монтгомери р' <— а х b' = a'b'f1 mod п и, наконец, выполним обратное преобразование: р <— р' х 1 = p'r~l = ab mod п. Можно обойтись и без обратного преобразования, если сразу вычислить р <— a xb, тем самым избавившись и от необходимости преобразо- вывать Ь. В результате получаем следующий алгоритм. Вычисление р =ab mod п (для нечетного и) с помощью произведения Монтгомери 1. Подобрать г := 2s такое, что 25-1 < п < 2s. Найти линейное пред- ставление 1 = r'r-п'п расширенным алгоритмом Евклида. 2. Положить а' <— ar mod п. 3. Вычислитьр «— a'xb, результат: р. Опять поясним алгоритм на маленьких числах. Пусть а = 123, b = 456, н = 789, г = 210. Тогда п =-n~{ modr = 963, «' = 501 и р = а' х b = 69 = ab mod п. Вычисление значений г и п на шагах 1, 2 и вычисление произведе- ния двух больших чисел все же требуют значительно больших вре- менных затрат, чем «обычное» модульное умножение, поэтому при
126 Криптография на Си и C++ в действии однократном модульном умножении алгоритмом Монтгомери пользоваться не стоит. Когда же нам нужно вычислить много модульных произведений по одному и тому же модулю, то есть когда указанные трудоем- кие предвычисления выполняются всего один раз, результаты более впечатляющие. Алгоритм Монтгомери особенно хорош для мо- дульного возведения в степень, нужно лишь немного изменить М-арный алгоритм. Опять представим показатель е и модуль п в системе счисления с основанием В = 2к: е = (ет-\ет_2---ео)в и п = Следующий алгоритм вычисляет степени ае mod п в кольце где п нечетное, с помощью произведения Монтгомери, Возведение в квадрат сводится к вычислению а х а. Возведение в степень по модулю п (для нечетного п) с помощью произведения Монтгомери 1. Положить г<—В/ = 2*/. Найти линейное представление \=г'г-пп расширенным алгоритмом Евклида. 2. Положить а <— ar mod п. Вычислить и запомнить а\а5, ...,а2 -1 с помощью произведения Монтгомери х в /?(г, и). 3. Если ет-1 * 0, то найти разложение ет_] = 2'u, где и нечетное. Положить р <— (ри J . Если em_i = 0, то положить р <— г mod п . В любом случае положить i <— т - 2. 4. Если =0, то положить =^...^p2jJ ...^ (к раз воз- вести в квадрат: р2 ~Р*Р). Если е,- * 0, то найти разложение е, = 2'и, где и нечетное. Поло- жить р <— [р2 xauJ . 5. При i > 0 положить i <— i - 1 и вернуться на шаг 4. 6. Результат: произведение Монтгомери Р х 1. Дальнейшее усовершенствование этого алгоритма возможно, скорее, за счет модификации алгоритма умножения Монтгомери, чем алго- ритма возведения в степень, что и сделали С.Р. Дуссе (S.R. Dusse) и Б.С. Калиски (B.S. Kaliski) в работе [DuKa]. Вычисляя произве- дение Монтгомери алгоритмом со стр. 125, можно избежать вы- полнения присваивания т <— tn mod г на шаге 2. Кроме того, прй выполнении преобразования Монтгомери можно оперировать с
6. Все дороги ведут к... модульному возведению в степень 127 и. nQ := п mod В, а не с п. Вычислим разряд mz <— tyi'o mod В, умно- жим его на и, затем на В1 и прибавим результат к t. Чтобы найти произведение чисел а, b <п по модулю п, представим, как и рань- ше, п - (п^пи.. .по)в и положим г := В1, rr - пп = 1 и н'о := п' mod В. к’ Алгоритм Дуссе и Калиски вычисления произведения Монтгомери axb 1. Положить t <— ab, и о и'mod В, i <— 0. 2. Вычислить ш, <— tyi'o m°d В (гщ будет одноразрядным целым числом). 3. Положить t <— t + пцпВ'. 4. Положить i <- i + 1; при i < I - 1 вернуться на шаг 2 5. Положить t <— t / г. • 6. Если t > п, то результат: t - п; иначе результат: t, В работе Дуссе и Калиски утверждается, что рассмотренное упро- щение основано на том, что t рассматривается как кратное числа г, однако доказательство не приводится. Прежде чем использовать эту процедуру, уточним, почему она действительно вычисляет про- изведение axb. Следующие рассуждения основаны на результатах Кристофа Бурникеля [Zieg]. На шагах 2 и 3 алгоритма вычисляется последовательность / по рекурсивной формуле: (6.16) t^ = ab, (6.17) Г0+1) !_ Bi i = Q ...J-l, Bl где Д7) = t + ((r mod B) (~n~x mod B) mod B) n - уже знакомая нам функция (см. формулу (6.13) при г<—В). Эле- (6.18) менты последовательности обладают следующими свойствами: = 0 mod В', (6.19) = ab mod и, (6.20) /о = abr 1 mod n, r
128 Криптография на Си и C++ в действцц (6.21) < 2n. r Свойства (6.18) и (6.19) следуют непосредственно из (6.14)—(6.17); из (6.18) получаем В1 |г(/) <=> г | г(/). Отсюда и из сравнения t(l) = ab mod п следует (6.20). Неравенство (6.21) выводим из соот- ношения r(/) =z(0)+n£W;B' <W, 1=0 поскольку /0) = ab < п2 < пВ1. Теперь скорость приведения по модулю определяется скоростью умножения чисел, по порядку величины близких к модулю. Такой вариант умножения по Монтгомери можно элегантно реализовать с помощью функции, аналогичной mulj (см. стр. 49). Функция: Умножение по Монтгомери Синтаксис: void mulmonj (CLINT aj, CLINT bj, CLINT nJ, USHORT nprime, USHORT logB_r, CLINT pj); Вход: aj, bj (сомножители а и b) nJ (модуль n > a, b) nprime (11 mod B) logB_r (логарифм числа г по основанию В = 216; должно выполняться неравенство в|09В-г"1 < п < В|О9В-Г) Выход: р_1 (произведение Монтгомери а х Ь = а • b • г"1 mod ri) i .* а • void mulmonj (CLINT aj, CLINT bj, CLINT nJ, USHORT nprime, USHORT logB.r, CLINT pj) { CLINTD tj; clint *tptrj, *nptrj, *tiptrj, *lasttnptr, *lastnptr; ULONG carry; USHORT mi; int i; mult (aj, bj, tj); lasttnptr = tj + DIGITS_L (nJ); lastnptr = MSDPTR_L (nJ);
ведут к... модульному возведению в степень 129 Используя функцию mult(), мы гарантируем отсутствие пере- полнения (см. стр. 86) при вычислении произведения чисел а_1 и b_l. Для возведения в квадрат по Монтгомери используем sqr(). Результат записывается в tj, где места для него достаточно. Затем t_l дополняется нулевыми старшими разрядами до длины, в два раза большей, чем длина nJ. for (i = DIGITS J_ (tj) + 1; I <= (DIGITS J_ (nJ) « 1); i++) { tJ[i] = O; } SETDIGITSJ. (tj, MAX (DIGITSJ_ (tj), DIGITS.L (nJ) « 1)); При выполнении следующего двойного никла последовательно вычисляются и складываются с t l частичные произведения т^пВ1, где mi := tjnz0. Здесь тоже текст программы аналогичен функции умножения. for (tptrj = LSDPTRJ. (tj); tptrj <= lasttnptr; tptrJ++) { carry = 0; mi = (USHORT)((ULONG)nprime * (ULONGftptrJ); for (nptrj = LSDPTFLL (nJ), tiptrj = tptrj; nptrj <= lastnptr; nptrj++, tiptrj++) { ‘tiptrj = (USHORT)(carry = (ULONG)mi ‘ (ULONG)‘nptrJ + (ULONG)‘tiptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); } В следующем внутреннем цикле при возникновении переполне- ния перенос записывается в старший разряд переменной tj, по- этому tj и содержит один дополнительный разряд. Этот шаг очень важен, поскольку в начале основного никла переменной tj присваивалось значение, в отличие от переменной pj, которая была инициирована путем умножения на 0.
130 Криптография на Си и C++ в действии for (; ((carry » BITPERDGT) > 0) && tiptrj <= MSDPTR.L (tj); tiptrj++) tiptrj = (USHORT)(carry = (ULONG)*tiptrJ + (ULONG)(USHORT)(carry » BITPERDGT)); if (((carry » BITPERDGT) > 0)) *tiptrj = (USHORT)(carry » BITPERDGT); INCDIGITS_L (tj); Далее следует деление на В1, для чего мы сдвигаем tj на logB_r бит вправо, т.е. отбрасываем младшие logB_r бит переменной tj. Затем модуль nJ при необходимости вычитается из tj, и tj воз- вращается в р_1 в качестве результата. tptrj = tj + (logB_r); SETDIGITSJ. (tptrj, DIGITSJ_ (tj) - (logB.r)); if (GE_L (tptrj, nJ)) sub J (tptrj, nJ, pj); + else cpyj (pj, tptrj); 1 . Функция sqrmonj() возведения в квадрат по Монтгомери несуш^" ственно отличается от только что рассмотренной: в вызове функции
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 131 нет переменной bj, а вместо функции умножения mult(a_l, bj, tj) используется функция sqr(aj, bj), точно так же пренебрегающая возможным переполнением. Однако здесь следует отметить, что при возведении в квадрат по Монтгомери вслед за вычислением р <— а х а выполняется обратное преобразование р <— р х 1 = р'г~{ = = a2 mod п (см. стр. 125). Функция: Возведение в квадрат по Монтгомери Синтаксис: void sqrmonj (CLINT aj, CLINT n_l, USHORT nprime, USHORT logB_r, CLINT pj); Вход: a_J (множитель a) nJ (модуль n > a) nprime (n mod B) logB_r (логарифм от г по основанию В = 216; должно выполняться неравенство в*°9В-г”1 < п < В|О9В_Г) Выход: р J (вычет а2 • г~{ по модулю и) В своей статье Дуссе и Калиски приводят также следующий вари- ант расширенного алгоритма Евклида (его мы еще будем рассмат- ривать в п. 10.2) для вычисления п0 = п mod В, который снижает сложность предвычислений. Этот алгоритм использует арифметику больших чисел для вычисления вычета -/Г1 mod 2Л, где 5 > 0. Алгоритм вычисления обратного значения -и"1 mod 2s, где s > 0, п нечетное 1. Положить х <— 2, у <— 1, i «— 2. 2. Если х < пу mod х, то положить у <— у + х. 3. Положить х<— 2х и i <— i + 1; при i < s вернуться на шаг 2. 4. Результат: х - у. Методом математической индукции можно доказать, что на шаге 2 рассмотренного алгоритма всегда выполняется сравнение уп = 1 mod х и, значит, у = /г-1 mod х. Как только на шаге 3 пере- менная х примет значение 2Л, мы получаем нужный результат: 2Л - у = -nl mod 2s, если только выбрать 5 из условия 2s = В. Этот алго- ритм реализован в виде небольшой функции invmomJO на FLINT/C. Аргументом функции является модуль п, результатом - значение -n~l mod В. Все эти соображения подтверждаются при построении функций mexp5mj() и mexpkmJO, для которых мы приводим здесь только интерфейс и численный пример.
132 Криптография на Си и C++ в действии Функция: Модульное возведение в степень в случае нечетного модуля (25-арный или 2*-арный метод с умножением по Монтгомери) Синтаксис: int mexp5m_l(CLINT basj, CLINT expj, CLINT pj, CLINT mJ); int mexpkmJ(CLINT basj, CLINT expj, CLINT pj, CLINT mJ); Вход: basj (основание) expj (показатель) mJ (модуль) Выход: pj (вычет по модулю mJ) Возврат: E_CLINT_OK, если все в порядке E_CLINT_DBZ в случае деления на 0 E_CLINT_MAL, если функция malloc() выдала ошибку EJ3LINT_MOD в случае четного модуля В этих функциях для вычисления произведения Монтгомери ис- пользуются процедуры invmonJO, mulmonj() и sqrmon_l(). В основе —- их реализации - функции mexp5_l() и mexpkJO, модифицированные в соответствии с описанным выше алгоритмом возведения в степень. । Поясним алгоритм возведения в степень по Монтгомери функцией , mexpkmJO на том же численном примере, который мы рассматри- вали при М-арном возведении в степень (см. стр. 116). Приведем основные этапы вычисления 1234667 mod 18577. 1. Предвычисления Представим показатель е = 667 в системе счисления с основанием 2к с к = 2 (см. алгоритм возведения в степень по Монтгомери на стр. 126), получим е= (1О1ОО11О11)22. Значение г, используемое в преобразовании Монтгомери, равно г = 216 = В = 65536. Значение п'о (см. стр. 127) теперь равно п0 = 34703. ; Преобразуем основание а в элемент системы вычетов R(r, л) чэг ч (см. стр. 124): а = ar mod п = ПМ • 65536 mod 18577 = 5743. Значение элемента а3 множества В(г, и) равно а3 = 9227. Показа- тель мал, поэтому дальнейшие степени числа а вычислять не нужно.
рддВАб. Все дороги ведут к... модульному возведению в степень 133 2. Основной цикл Разряд показателя е, = 2'и 2'-1 2’-1 2°-1 2’-1 2°-3 р<-р2 - 16994 3682 14511 11066 la. г la. - - 6646 - 12834 р рхаи 5743 15740 8707 16923 1583 т 9025 11105 - 1628 - 3. Результат Значение р после обратного преобразования: р = р х 1 = pr~l mod и = 1583г"1 mod и = 4445. Читателя, интересующегося подробностями программной реализа- ции функций mexp5m_l() и техркт_1()и численного примера для функции mexpkmJO, мы отсылаем к исходному тексту программы на FLINT/C. В начале главы мы ввели функцию wmexp_l(), удобную в случае малых оснований и требующую только умножений вида р pa mod т переменных типа CLINT * USHORT mod CLINT. Эту функцию также можно ускорить, заменив модульное возведение в квадрат аналогичной процедурой по Монтгомери, как мы это делали в mexpkmJO. Здесь мы будем использовать быструю функцию об- ращения invmonJO, а умножение оставим без изменений. Это можно сделать, поскольку при возведении в квадрат по Монтгомери и обычном умножении по модулю п (а2г-1) b = (а2/?) г-1 mod п мы остаемся в рамках системы вычетов /?(г, л) = {ir mod п | 0 < i < п}. В результате получаем две функции: wmexpmJO и umexpmJO, аргументами которых являются показатели типа USHORT, для нечетных модулей. Эти функции являются значительно более бы- стрыми по сравнению с «обычными» функциями wmexpJO и umexpJO. Для них мы снова приводим лишь интерфейс и числен- ный пример, а читателя отсылаем за подробностями к исходному тексту программы на FLINT/C.
134 Криптография на Си и C++ в действии функция: Модульное возведение в степень с использованием преобразования Монтгомери для основания (или показателя соответственно) типа USHORT и нечетного модуля Синтаксис: int wmexpmj (USHORT bas, CLINT ej, CLINT pj, CLINT mJ); int umexpmj (CLINT basj, USHORT e, CLINT pj, CLINT mJ); Вход: bas, basj (основание) e, ej (показатель) mJ (модуль) Выход: pj (вычет baseJ по модулю mJ, соответственно вычет basje по модулю mJ) Возврат: E_CLINTJ3K, если все в порядке E_CLINT_DBZ в случае деления на 0 E_CLINT_MOD в случае четного модуля Функция wmexpmJ заготовлена специально для алгоритма проверки на простоту из п. 10.5, где мы, наконец-то, пожнем плоды тепереш- них усилий. Проиллюстрируем эту функцию на уже знакомом нам примере: вычислим 1234667 mod 18577. 1. Предвычисления и . Двоичное представление показателя: = (1010011011 )2. Значение г, используемое в преобразовании Монтгомери: : г=2,6 = В = 65536. Значение п'о (см. стр. 127) вычисляется, как и раньше: п'о = 34703. Определяем начальное значение р <— pr mod 18577. 2. Основной цикл Двоичный разряд показателя 1 0 1 0 0 1 1 0 1 1 р рхр в 7?(г, /z) 9805 9025 16994 11105 3682 6646 14511 1628 11066 9350 р ь-~ра mod /z 5743 - 15740 - - 8707 16923 1349 1583 3. Результат Значение р после обратного преобразования: р = р х 1 = pr~l mod/z = 1583г-1 mod/z = 4445. Подробное исследование временной сложности преобразования Монтгомери с учетом различного рода оптимизаций можно найти в работе [Boss]. Мы обещали читателю 10-20%-ный выигрыш в ско- рости от использования преобразования Монтгомери по сравнению с традиционным возведением в степень. Приложение D, в котором
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 135 приведены типичные оценки времени для функций на FLINT/C, полностью подтверждает наши обещания. Мы ограничились случа- ем возведения в степень по нечетному модулю. Тем не менее, для многих прикладных задач, например, при зашифровании и рас- шифровании, а также при вычислении цифровой подписи по алго- ритму RSA (см. главу 16) можно воспользоваться функциями mexp5mj() и mexpkmj(). Подведем итоги. В нашем распоряжении несколько функций мо- дульного возведения в степень. Сведем их в таблицу 6.5 с учетом особенностей и возможных областей применения. Таблииа 6.5. Функция Область применения функции возведения в степень в пакете FLINT/C mexp5J() Обшее 25-арное возведение в степень; не использует динамическое распределение памяти; значительные требования к стеку. р mexpkJO Обшее 2к-арное возведение в степень, где значение к оптимально для чисел типа CLINT; использует динамическое распределение памяти; незначительные требования к стеку. ') • mexp5mJ0 25-арное возведение в степень по Монтгомери для нечетного модуля; не использует динамическое распределение памяти; значительные требования к стеку. mexpkmJO 2^-арное возведение в степень по Монтгомери для нечетного модуля, где значение к оптимально для чисел типа CLINT длиной до 4096 двоичных разрядов; использует динамическое распределение памяти; незначительные требования к стеку. umexpJO Смешанное бинарное возведение в степень для основания типа CLINT, показателя типа USHORT; незначительные требования к стеку. umexpmJO Смешанное бинарное возведение в степень с исполь- зованием преобразования Монтгомери для основания типа CLINT, показателя типа USHORT и только для нечетного модуля; незначительные требования к стеку. wmexpJO Смешанное бинарное возведение в степень для основания типа USHORT, показателя типа CLINT; незначительные требования к стеку. wmexpmJO Смешанное бинарное возведение в степень с использованием возведения в квадрат по Монтгомери для основания типа USHORT, показателя типа CLINT и нечетного модуля; незначительные требования к стеку. mexp2J() Смешанное возведение в степень с показателем вида 2е; незначительные требования к стеку.
136 Криптография на Си и C++ в действии 6.5. Криптографические приложения модульного возведения в степень На протяжении этой главы мы славно поработали над модульным возведением в степень. Пора бы остановиться и спросить себя: а для чего оно нужно в криптографии? Первое, что приходит в голову, это, несомненно, вычисления в криптосистеме RSA, где показате- : лями степени при зашифровании и расшифровании являются, соот- ветственно, открытый и секретный ключи. Но пусть читатель набе- рется немного терпения, поскольку для изучения криптосистемы RSA у нас в следующей главе припасено кое-что еще, так что от- ложим разговор до главы 16. Для самых нетерпеливых приводим два очень важных алгоритма, в * которых нужно возводить в степень. Это протокол обмена ключами, предложенный в 1976 году Мартином Е. Хеллманом (Martin Е. Hellman) и Уитфилдом Диффи (Whitfield Diffie) [Diff], и его обоб- щение - протокол шифрования с открытым ключом Тахира Эль- u i > ’ . Гамаля (Taher ElGamal). Протокол Диффи - Хеллмана стал прорывом в области крипто- графии. Это первая в истории криптосистема с открытым ключом, или, иначе, асимметричная криптосистема (см. главу 16). Спустя vV’r ? два года Ривест (Rivest), Шамир (Shamir) и Адлеман (Adleman) опубликовали процедуру RSA (см. работу [Rive]). Сегодня различ- ные разновидности протокола Диффи-Хеллмана используются для ь о распределения ключей в Интернете и в защищенных протоколах безопасности IPSec, IPv6 и SSL, предназначенных для безопасной передачи пакетов на уровне протоколов и для передачи данных на прикладном уровне (например, при электронных платежах). Трудно переоценить практическую значимость этого принципа распреде- ления ключей.2 Два участника протокола Диффи-Хеллмана, назовем их, к примеру, А и В, могут очень просто установить секретный сеансовый ключ, который впоследствии может быть использован для зашифрования передаваемых сообщений. Сначала А и В выбирают большое про- стое число р и примитивный корень а по модулю р (мы еще вер- немся к этому позже). Протокол Диффи-Хеллмана выглядит так. Протокол IP Security (IPSec), разработанный Рабочей группой инженеров Internet (Internet Engi- neering Task Force; IETF), представляет собой всесторонне защищенный протокол и является ча- стью будущего Internert-протокола IPv6. При разработке учитывалась возможность использова- ния этого протокола и в текущем IPv4. Протокол безопасных соединений Secure Socket Layer (SSL) разработан компанией Netscape на основе протокола TCP, обеспечивает оконечный безо- пасный обмен в приложениях HTTP, FTP и SMTP (обо всем этом см. [Stal], Главы 13 и 14).
j-ддВА 6. Все дороги ведут к... модульному возведению в степень 137 Протокол обмена ключами Диффи-Хеллмана 1. А выбирает произвольное число хь<р- 1 и отправляет В свой открытый ключ уд := а А. 2. В выбирает произвольное число хв < р - 1 и отправляет А свой открытый ключ ув := а в. 3. А вычисляет секретный ключ за := у*А mod р. 4. В вычисляет секретный ключ зв := ух* mod р. Поскольку выполняется равенство з. - У** = « ГвЛа = yfB = з„ mod р , после шага 4 у А и у В оказывается один и тот же сеансовый ключ. Значения аир, как и значения за и зв, передаваемые на шагах 1 и 2, могут быть несекретными. Безопасность этого протокола зависит от сложности задачи дискретного логарифмирования в конечных полях, а взлом криптосистемы эквивалентен задаче вычисления значений ха и хв, зная значения уд и ув в 3 Утверждение о том, что вычислить аху, зная ах и ау, в конечной циклической группе (задача Диффи-Хеллмана) так же трудно, как вычислить дискрет- ные логарифмы, и, следовательно, об эквивалентности этих задач, общепринято, но не доказано. Таким образом, чтобы гарантировать безопасность протокола, нужно выбирать число р достаточно большим (не менее 1024 бит, еще лучше 2048 и больше; см. таблицу 16.1). Кроме того, число р - 1 должно иметь большой простой делитель, близкий к числу (р- 1)/2, чтобы нельзя было применить специальные методы реше- ния задачи дискретного логарифмирования (процедура выбора таких простых чисел будет рассмотрена в главе 16, где мы поговорим о генерации сильных простых чисел для криптосистемы RSA). Достоинством протокола Диффи-Хеллмана является то, что сек- ретные ключи можно вырабатывать по мере надобности и не нужно хранить в течение длительного времени. Кроме того, для использо- вания протокола не требуется никаких дополнительных элементов для согласования параметров а и р. У этого протокола есть и недос- татки, самый серьезный из которых - отсутствие доказательства подлинности предаваемых параметров уд и ув. Это делает протокол уязвимым по отношению к «человек посередине», когда наруши- тель X перехватывает истинные открытые ключи уд и ув и заменяет их своим поддельным ключом ух. В этом случае А и В вычисляют «секретные» ключи з' .- у*А mod р и з' .- у* mod р , а X, в свою 3 О задаче дискретного логарифмирования см. работы [Schn], п. 11.6 и [Odly].
138 Криптография на Си и C++ в действии очередь, вычисляет ключ s'k ухк = ах*х* = ах*х* = у** = s'k mod р и, аналогично, ключ 5' . Теперь вместо одного протокола между А и В получается два протокола: между X и А и между X и В. Таким об- разом, нарушитель X может перехватить сообщение, переданное участником А, дешифровать его и отправить участнику В поддель- ное сообщение (то же в обратном направлении). Катастрофа заклю- чается в том, что с точки зрения криптографии А и В даже не будут подозревать о том, что произошло. Для использования в Интернете были предложены различные вари- анты и обобщения протокола Диффи-Хеллмана, в которых не- сколько сглажены недостатки и одновременно сохранены достоин- ства этого протокола. Какова бы ни была версия этого протокола, всегда следует обеспечить возможность проверки подлинности ключевой информации. Это можно сделать, например, так. Участ- ники протокола подписывают цифровой подписью свои открытые ключи и вместе с ключом посылают сертификат, выданный серти- фицирующим органом (см. стр. 374, п. 16.3) с использованием про- токола SSL. В протоколах IPSec и Ipv6 реализована сложная проце- дура под названием ISAKMP/Oakley,4 в которой устранены все не- достатки протокола Диффи-Хеллмана (подробности см. в работе [Stal], стр. 422-423). Определить примитивный корень по модулю р (иначе - первооб- разную), то есть такое число а, множество степеней которого a! mod р, i = 0, 1, ..., р - 2, совпадает с мультипликативной группой = {1, ...,р- 1} (см. п. 10.2), можно следующим алгоритмом (см. [Knut], п. 3.2.1.2, теорема С). Предполагается, что известно разло- жение на простые множители числа р - 1 - порядка мультиплика- тивной группы р -1 = рр ... р[к . Вычисление примитивного корня по модулю р 1. Выбрать случайное целое число а из интервала [0, р - 1] и по- ложить i <- 1. 2. Вычислить t <г- a{p^Pi mod р . 3. Если t = 1, то вернуться на шаг 1. Иначе положить i <— i + 1. При i < k вернуться на шаг 2. При i > k результат: а. Реализуем алгоритм в виде следующей функции.
|-ддВА 6. Все дороги ведут к... модульному возведению в степень 139 I—— функция: Генерация примитивного корня по модулю р (р > 2 и простое) Синтаксис: int primrootj (CLINT aj, unsigned noofprimes, clint **primesj); Вход: noofprimes (число различных простых делителей числа р - 1 - по- рядка мультипликативной группы) primesJ (вектор указателей на CLINT-объекты, сначала идет р - 1, а затем простые делителирь ..рк числа р-1 = р^ ...ркк , к = noofprimes) Выход: а_1 (первообразный корень по модулю pj) Возврат: E_CLINT_OK, если все в порядке -1, если число р - 1 нечетное и, следовательно, число р составное int primrootj (CLINT aj, unsigned int noofprimes, clint *primesj[]) ж. { CLINT pj, tj, junkj; ULONG i; ..0)1 '.«ж- if (ISODDJ- (primesJ[0])) return -1; primes J[0] содержит число p - 1, из которого мы получаем мо- л / дуль в р_1: cpyj (pj, primesJ[0]); inc J (pj); SETONE.L (aj); do { inc J (aj);
140 Криптография на Си и C++ в действии Искомый примитивный корень а - это натуральное число, боль- шее либо равное 2, поэтому рассматриваем только такие числа. Если а является полным квадратом, то оно не может быть перво- образным корнем, поскольку тогда а(р1)/2 = 1 mod р и порядок элемента а меньше, чем 0(р) = р -1. В этом случае увеличиваем значение переменной aj. Проверка того, является ли aj полным квадратом, выполняется с помошью функции issqrJO (см. п. 10.3). if (issqrj (aj, t_l)) { inc_l (aj); } Значение t <— a^p x^Pi mod p вычисляем в два этапа. Проверяем все простые делители р; по очереди; используем возведение в степень по Монтгомери. Найденный первообразный корень за- писывается в а_1. do { divj (primes_J[0], primesJ[i++], tj, junkj); mexpkmj (aj, tj, tj, pj); while ((i <= noofprimes) && !EQONE_L (t_l)); } while (EQONEJ- (tJ)); return E_CLINT_OK; } Еще одним примером приложения, использующего возведение в степень, является протокол шифрования Эль-Гамаля. Этот прото- кол является обобщением протокола Диффи-Хеллмана, его стой- кость также определяется сложностью задачи дискретного лога- рифмирования, то есть взлом протокола эквивалентен решению за- дачи Диффи-Хеллмана (см. стр. 137). С помощью протокола Эль- Гамаля осуществляется управление ключами во всемирно известной
ГЛАВА 6. Все дороги ведут к... модульному возведению в степень 141 системе Pretty good privacy (PGP), разработанной Филом Циммер- маном (Phil Zimmermann) и используемой для зашифрования и подписи сообщений электронной почты и электронных документов (см. [Stal], п. 12.1). Л. Участник А выбирает открытый и соответствующий секретный ключ следующим образом. Генерация ключей для протокола Эль-Гамаля 1. Участник А выбирает большое простое число р такое, что число р- 1 имеет большой простой делитель, близкий к (р - 1)/2 (см. стр. 363), и примитивный корень а из мультипликативной группы Zp*, как указано выше (см. стр. 138). 2. Участник А выбирает случайное число х такое, что 1 < х < р - 1, и вычисляет b := ах mod р с помощью алгоритма Монтгомери. 3. Открытым ключом участника А является тройка (р, а, соот- ветствующим секретным ключом - тройка (р, а, х)А- •‘НХ- Теперь участник В с помощью тройки (р, а, Ь)а может зашифровать сообщение Me {1, р- 1} и отправить его участнику А. Прото- кол выглядит так. я Протокол Эль-Гамаля шифрования с открытым ключом 1. Участник В выбирает случайное число у такое, что 1 < у < р - 1. 2. Участник В вычисляет а := ау mod р и Р := Mix mod р = М(ахУ mod р. 3. Участник В отправляет участнику А шифрограмму С := (а, Р). 4. Участник А вычисляет из шифрограммы С открытый текст: М = p/ocv mod р. Рассмотренная процедура корректна, поскольку — = д/-----= М mod р . аЛ (ау)х (ах)у Значение р/осЛ вычисляется как произведение РосГ1"* по модулю р. Размер числа р должен быть не менее 1024 бит, в зависимости от приложения (см. таблицу 16.1). Кроме того, при зашифровании двух разных сообщений и М2 следует использовать разные слу- чайные числа yi у2, поскольку в противном случае равенство _ M,by = Мх Р2 М2ЬУ “ М2
142 Криптография на Си и C++ в действии позволяет из сообщения вычислить сообщение М2. При практи- ческой реализации этого протокола нужно учитывать, что шифро- грамма С в два раза длиннее, чем открытый текст М, то есть объем передаваемых данных здесь больше, чем в других протоколах. •'•ХЭэ Протокол Эль-Гамаля в том виде, как он приведен здесь, обладает весьма любопытным недостатком, благодаря которому нарушитель может получить сведения об открытом тексте, располагая лишь незначительным объемом информации. Циклическая группа on- V '< содержит подгруппу U := {дх| число х четное} порядка (р - 1)/2 (см. [Fisc], глава 1). Если, b = ах или ос = ау - элемент подгруппы U, то и аху - элемент подгруппы U. Если еще и шифртекст 0 является элементом подгруппы U, то М = тоже принадлежит подгруппе U. Аналогичное рассуждение справедливо и в том случае, если ни аху\ ни Р не лежат в подгруппе U. В двух оставшихся случаях, - ко- гда в лишь один из элементов аху и Р не лежит в U, - открытый ОЭ ’ текст М также не лежит в U. Распознать эту ситуацию позволяет следующий критерий: 1. аху g U <=> (ах g U или ау е U). Эту ситуацию, как и то, лежит ли Р в U,можно проверить так: 2. Для любого и е включение ие U выполняется тогда и только тогда, когда = 1. ЖН Насколько же серьезно то, что нарушитель получит такую инфор- - Ц >v 'г г мацию о сообщении Л7? С точки зрения криптографии это совер- шенно неприемлемо, поскольку в этом случае множество сообще- ний, по которому ведется перебор, легко сокращается вдвое. На практике допустимость такой ситуации определяется приложением. Отсюда становится понятно, почему не стоит скупиться при выборе } длины ключа. C'VQM VI Можно предпринять еще некоторые шаги по устранению указанного недостатка, если не бояться внести новые и неизвестные. На шаге 2 умножение Mb? mod р можно заменить зашифрованием V(H(axy), М) с помощью подходящего симметричного алгоритма V (это может быть тройной DES, IDEA или новый стандарт шифрования AES; см. главу 19) и хэш-функции Н (см. стр. 373), сжимающей значение аху до размера ключа в алгоритме V. 1 ' Разумеется, это далеко не все приложения, в которых может исполь- зоваться модульное возведение в степень. В теории чисел (а значит, и в криптографии) это стандартная операция, и с ней мы еще не раз встретимся в дальнейшем, особенно в главах 10 и 16. Множество прикладных примеров можно найти в работе [Schr] и, конечно, в энциклопедических трудах [Schn] и [MOV].
Г Л А В A 7. Поразрядные и логические функции - ... совсем наоборот, - подхватил Труляля. - Если бы это было так, это бы ещё ничего, а если бы ничего, оно бы так и было, но так как это не так, так оно и не этак. Такова ло- гика вещей. Льюис Кэрролл, Алиса в Зазеркалье, (Перевод с английского Н. Демуровой) В этой главе мы представим функции, реализующие поразрядные операции над CLINT-объектами, а также познакомимся поближе с функциями, которыми мы уже отчасти пользовались - для опреде- ления равенства и размера CLINT-объектов. К поразрядным функциям относятся и операции сдвига, которые сдвигают CLINT-аргумент в двоичном представлении, изменяя по- зиции отдельных битов, и некоторые другие функции от двух CLINT-аргументов, дающие возможность непосредственно работать с двоичным представлением CLINT-объектов. То, как эти функции можно применить в арифметических целях, наиболее наглядно по- казывают операции сдвига, описываемые ниже. Мы также видели в п. 4.3, как можно использовать поразрядную операцию AND для приведения по модулю, равному степени двойки. 7.1. Операции сдвига Всем движет необходимость. Франсуа Рабле Самый простой способ умножить число а, представленное по осно- ванию В в виде а = (ап-1ап.2...а0)в, на Ве ~ это сдвинуть а влево на е разрядов. Для двоичного представления это происходит точно так же, как и для хорошо нам известной десятичной системы: аВ — (ап+е-\ап+е.-2- • где ^п+е-1 » ^п+е-2 &п-2’ • • •» 0, • • •, CIq 0. Для В - 2 это соответствует умножению числа в двоичном пред- ставлении на 2 для В = 10 - умножению на степень десяти в деся- тичной системе.
144 Криптография на Си и C++ в действии ] 1 ’Ч 3 аналогичной процедуре для целочисленного деления на степень 3 разряды числа сдвигаются вправо: а . е \^п-1 • • •^п-е^п-е-\^п-е-2’ • *^0/Zb В ще L/-1 — ... — С1п~е — 0, С1п_е_\ — С1п-\, С1п_е_2 — йл_2, .. а0 — ае. J Цля В = 2 это соответствует целочисленному делению числа в дво- е 4чном представлении на 2 , для других оснований получается ана- югичный результат. ] Гак как разряды CLINT-объектов в памяти представлены в двоич- ном виде, эти объекты можно легко умножать на степени двойки юсредством сдвига налево всех разрядов поочередно, при этом ос- гавшиеся справа пустые разряды заполняются нулями. Аналогичным образом CLINT-объекты можно делить на степени двойки, сдвигая каждый двоичный разряд вправо в сторону млад- лих разрядов. Оставшиеся в конце свободные разряды или запол- няются нулями, или игнорируются как ведущие нули. При этом на <аждом шаге (сдвиге на один разряд) самый младший разряд те- ряется. Преимущества этого процесса очевидны. Процедуры умножения и деления CLINT-объектов на 2 легко выполнимы и требуют не более 4"logBa"| операций сдвига, чтобы переместить каждую величину j Л ' j ] гипа USHORT на один двоичный разряд. Умножение и деление на 5 использует только |~logBa"] операций для записи USHORT- зеличин. ( . W' J м * j НГО” ( ] ( 1 ( Цалее мы рассмотрим три функции. Функция shl_1() выполняет бы- строе умножение CLINT-числа на 2, а функция shrJQ выполняет де- 1ение CLINT-числа на 2 и возвращает целое частное. И, наконец, функция shiftJO умножает или делит число а типа е 3LINT на 2 . Вид выполняемой операции определяется знаком пока- зателя степени е, который передается функции в качестве аргумента. Если показатель положительный, выполняется умножение, если отрицательный - деление. Если е имеет представление е = В- k + /, ' < В, то функция shiftJO выполняет умножение или деление за 7 + 1) [~logBfl~] операций над USHORT-величинами. ] ( ( I I Зсе три функции выполняют вычисления над объектами типа 3LINT по модулю (Утах + 1)- Они реализованы как функции- сумматоры, то есть изменяют значение своих операндов, записывая з операнд результат вычислений. Функции проверяют наличие лереполнения и потери значимости. Однако при сдвигах потери d
рдДВА 7. Поразрядные и логические функции 145 значимости не возникает, так как если величина сдвига оказыва- ется больше количества разрядов, то в результате получается нуль. В этом случае значение состояния E_CLINT_UFL для потери значи- мости просто показывает, что было меньше сдвигов, чем требова- лось. Другими словами, степень двойки, выступающая в качестве делителя, оказалась больше, чем делимое, и поэтому частное рав- няется нулю. Три указанные функции реализованы следующим образом. функция: Сдвиг влево (умножение на 2) Синтаксис: int shlj (CLINT aj); Вход: a J (множитель) Выход: aj (произведение) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int shlj (CLINT aj) г 1 t clint *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++) { *apj = (USHORT)(carry = ((ULONG)*apJ « 1) | (carry » BITPERDGT));
146 Криптография на Си и C++ в действии if (carry » BITPERDGT) { if (DIGITS.L (aj) < CLINTMAXDIGIT) { *ap J = 1; SETDIGITSJ- (aj, DIG ITS J. (aj) + 1); error = E_CLINT_OK; } else I { i error = E_CLINT_OFL; } L— } RMLDZRSJ. (aj); return error; fl } Функция: Сдвиг вправо (целочисленное деление на 2) Синтаксис: int shrj (CLINT a J); Вход: aj (делимое) Выход: a J (частное) Возврат: E_CLINT_OK - если все в порядке E_CLINT_UFL - в случае потери значимости int shr_l (CLINT aj) { clint ‘apj; USHORT help, carry = 0; if (EQZ_L (aj)) return E_CLINT_UFL; for (ap_l = MSDPTR_L (a_J); apj > aj; ap_l-)
j-ддВА 7. Поразрядные и логические функции 147 ' HU { help = (USHORT)((USHORT)(*apJ » 1) | (USHORT)(carry « (BITPERDGT-1))); -к carry = (USHORT)(*apJ & 1U); *apj = help; J RMLDZRS.L (а_1); return E_CLINT_OK; Функция: Сдвиг влево/вправо (умножение/деление на степень двойки) Синтаксис: int shift J (CLINT nJ, long int noofbits); Вход: nJ (операнд), noofbits (показатель степени двойки) Выход: nJ (произведение или частное, в зависимости от знака noofbits) Возврат: E_CLINT_OK, если все в порядке E_CLINT_UFL в случае потери значимости E_CLINT_OFL в случае переполнения int shift J (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; clint *nptrj; clint *msdptrnj; RMLDZRSJ. (nJ); resl = (int) IdJ (nJ) + noofbits;
148 Криптография на Си и C++ в действии П ; Если nJ == 0, нужно лишь правильно установить код ошибки и работа закончена. Аналогично для случая noofbits == 0: if (*nj == 0) { return ((resl < 0) ? E_CLINT_UFL : E_CLINT_OK); 1 if (noofbits == 0) Я { . . 1 return E_CLINT_OK; ( 1 и Далее проверяется наличие переполнения или потери значимости. Затем, в зависимости от знака noofbits, выбирается сдвиг влево или вправо: if ((resl < 0) || (resl > (long) CLINTMAXBIT)) 1 I error = ((resl < 0) ? EJDLINTJJFL : E_CLINT_OFL); /* Потеря значимости или переполнение?*/ Ц 1 if (noofbits < 0) { Если noofbits < 0, тогда nJ делится на 2noofb,ts. Число сдвигаемых разрядов в nJ ограничено DIGITSJ. (nJ). Сначала сдвигаются целые разряды, а затем оставшиеся биты с помошью shrj(): shorts = MIN (DIGITSJ_ (nJ), shorts); msdptrnj = MSDPTRJ- (nJ) - shorts; for (nptrj = LSDPTFLL (nJ); nptrj <= msdptrnj; nptrj++)
рддВА 7. Поразрядные и логические функции 149 ‘nptrj = ‘(nptrj + shorts); SETDIGITSJ. (nJ, DIGITS J. (nJ) - (USHORT)shorts); for (I = 0; I < bits; I++) { shrj (nJ); } } else ' ' . "'I \ / i T Если noofbits > 0, to n_l умножается на 2noofb,ts. Если число shorts сдвигаемых разрядов больше, чем МАХВ, тогда результат равен нулю. В противном случае сначала определяется и сохраняется число разрядов нового значения и затем сдвигаются целые раз- ряды, а освободившиеся разряды заполняются нулями. Чтобы избежать переполнения, начальная позиция ограничивается n_l + МАХВ и хранится в nptrj. Как и раньше, последние биты сдвигаются по отдельности с помошью shl J(): if (shorts < CLINTMAXDIGIT) { SETDIGITS_L (nJ, MIN (DIGITS_L (nJ) + shorts, CLINTMAXDIGIT)); nptrj = nJ + DIGITSJ_ (nJ); msdptrnj = nJ + shorts; while (nptrj > msdptrnj) { *nptrj = *(nptrj - shorts); -nptrj; } while (nptrj > nJ) {
150 Криптография на Си и C++ в действие *nptr_l- = 0; } RMLDZRSJ. (nJ); for (i = 0; i < bits; i++) { shlj (nJ); } } else { SETZERCJL (nJ); } return error; 7.2. Все или ничего: битовые соотношения Пакет FLINT/C содержит функции, позволяющие использовать бинарные С-операторы &, |, Л и для типа CLINT. Однако прежде чем рассмотреть программы, реализующие эти функции, хотелось бы понять, что нам дает их реализация. С математической точки зрения мы рассматриваем соотношения обобщенных булевых функций /: {0,1}^ —» {0,1}, которые отобра- жают набор из к чисел (хь ..., хк) 6 {0, 1 }* в 1 или в 0. Действие булевых функций обычно представляется таблицей значений (см. таблицу 7.1). Таблииа 7.1. Значения булевых функиий X] х2 хк Лхъ хк) 0 0 0 0 1 0 0 1 0 1 0 0 1 1 1 1
рддВА 7; Поразрядные и логические функции 151 В случае битовых соотношений между CLINT-типами сначала бу- дем рассматривать в качестве переменных вектора битов (хь ...»хл), и формировать значения булевых функций последовательно. Таким образом, мы имеем функции /:{0, 1}"х{0, 1}л—>{0, 1}", < / 1 1 к которые отображают n-битовые переменные х1 :=(хр х2,..., хл) и 2 2 2 х2 := (%!> *„) в п-битовую переменную (хь хп) следующим образом: - 12 где/(хь х2) :=/л, xz). Полученный таким образом вектор (хь ..хп) понимается в дальнейшем как число типа CLINT. Решающее значение при выполнении функции f имеет описание час- тичных функций f, которые определены в терминах булевой функ- ции/. Булевы функции, реализуемые CLINT-функциями and_l(), ог_1() и хог_1(), задаются следующим образом (см. таблицы 7.2 - 7.4). Таблииа 7.2. Значения Ху Х2 fUi, х2) 0 0 0 CLINT-функиии and_l() 0 1 0 1 0 0 1 1 1 Таблииа 7.3. Значения Ху х2 Лху, х2) 0 0 0 CLINT-функиии orj() 0 1 1 1 0 1 1 1 1 Таблииа 7.4. Значения Ху х2 /Ixi, х2) 0 0 0 CLINT-функиии xorj() 0 1 1 1 0 1 1 1 1
152 Криптография на Си и C++ в действии : ' ?' 1 U4 ' Реализация этих булевых функций тремя С-функциями andjQ or_l() и хог_1() происходит не поразрядно, а посредством обработку разрядов CLINT-переменных стандартными С-операторами &, | и а Каждая из этих функций допускает три аргумента типа CLINT, при. чем первые два являются операндами, а последний - результирую- щей переменной. Функция: Реализация поразрядного AND Синтаксис: void and J (CLINT aj, CLINT bj, CLINT cj); Вход: a J, b_l (обрабатываемые аргументы) Выход: cJ (результат операции AND) void andj (CLINT a_l, CLINT bj, CLINT c_l) 1 1 CLINT dj; I clint *rj, *s_l, *tj; R clint *lastptr_l; - ’ о ь Вначале указатели r_l и s_l устанавливаются на соответствующие разряды аргументов. Если аргументы имеют разное количество разрядов, то s_l указывает на аргумент меньшей длины. Указатель msdptraj - на последний разряд этого аргумента. if (DIGITS.L (а_1) < DIGITS.L (bj)) { rj = LSDPTR.L (bj); sj = LSDPTR.L (aj); lastptrj = MSDPTR.L (a J); } else { 1 rj = LSDPTFLL (a_l); ; | s_l = LSDPTFLL (bj); i ; lastptrj = MSDPTFLL (bj); b
В A 7. Поразрядные и логические функции 153 Теперь указателю tj ссылается на значение первого разряда ре- зультата, а максимальная длина результата хранится в dJ[O]: tj = LSDPTRJ- (dj); SETDIGITS_L (dj, DIGITS.L (sj - 1)); Сама операция выполняется в следующем цикле над разрядами к 1 аргумента меньшей длины. При этом результат не может иметь большее количество разрядов. while (sj <= lastptrj) { *tj++ = *rj++ & *sj++; } После того как результат переписывается в с_1 (ведушие нули при этом отбрасываются), функция заканчивает работу. cpyj (сJ, dj); } Функция: Реализация поразрядного OR Синтаксис: void orj (CLINT aj, CLINT bj, CLINT cj); Вход: aj, bj (обрабатываемые аргументы) Выход: cj (результат операции OR) void orj (CLINT aj, CLINT bj, CLINT cj) { CLINT dj; clint *rj, *sj, *tj; clint *msdptrrj; clint *msdptrsj;
154 Криптография на Си и C++ в действие Указатели rj и s_l задаются так же, как и выше. if (DIGITS.L (aj) < DIGITSJ- (bJ)) rj = LSDPTR.L (bj); sj = LSDPTFLL (aj); msdptrrj = MSDPTRJ. (bj); msdptrsj = MSDPTR.L (aJ); } else { rj = LSDPTR.L (aj); sj = LSDPTR.L (bj); msdptrrj = MSDPTRJ. (aj); msdptrsj = MSDPTRJ. (bj); tj = LSDPTR.L (dj); SETDIGITS.L (dj, DIGITS.L (rj - 1 )); Сама операция происходит в цикле над разрядами аргумента меньшей длины: while (sj <= msdptrsj) { *t_l++ = *r_l++ | *s_l++; } Далее берутся остающиеся разряды аргумента большей длины. После того как результат переписывается в с_1 (с отбрасыванием начальных нулей), функция завершает работу: while (rj <= msdptrrj)
7. Поразрядные и логические функнии 155 1 { *tj++ = *г_1++; } cpyj (cj, dj); } функция: Реализация поразрядного исключающего OR (XOR) Синтаксис: void xorj (CLINT aj, CLINT bj, CLINT cj); Вход: aj, bj (обрабатываемые аргументы) Выход: cJ (результат операции XOR) void xorj (CLINT aj, CLINT bj, CLINT cj) { CLINT dj; clint *rj, *sj, *tj; clint *msdptrrj; clint *msdptrs_l; if (DIGITS_L (aj) < DIGITS_L (bj)) { rj = LSDPTFLL (bj); sj = LSDPTFLL (aj); msdptrrj = MSDPTFLL (bj); msdptrsj = MSDPTFLL (aj); } else { rj = LSDPTR J. (a_l); sj = LSDPTFLL (bj); msdptrrj = MSDPTFLL (aj); msdptrsj = MSDPTFLL (bj); } tj = LSDPTFLL (dj); SETDIGITSJ_ (dj, DIGITS_L (rj - 1));
156 w Криптография на Си и C++ в действци Теперь выполняется непосредственно операция. Данный никл обрабатывает разряды аргумента меньшей длины. while (s_l <= msdptrsj) { *tj++ = *r_l++ л *sj++; } Оставшиеся разряды другого аргумента переписываются, как ука- зано выше: while (r_l <= msdptrrj) { *t_l++ = *rj++; } cpyj (c_l, dj); } Функцию andJO можно использовать для приведения числа а по мо- дулю степени двойки 2^, задавая CLINT-переменной а_1 значение а, CLINT-переменной bj значение 2^ - 1 и вычисляя andj (aj, b_l, cj). Однако эту операцию можно выполнить быстрее с помощью соз- данной для этой цели функции mod2J(), в которой учитывается тот факт, что двоичное представление числа 2^ - 1 состоит исключи- тельно из единиц (см. п. 4.3). 7.3. Прямой доступ к отдельным двоичным разрядам В некоторых случаях оказывается полезной возможность обра- щаться непосредственно к отдельным двоичным разрядам для того, чтобы прочитать или изменить их. В качестве примера можно упо- мянуть присваивание CLINT-объекту значения степени 2, которое легко осуществляется заданием значения одного бита. Далее мы подробно рассмотрим три функции, setbitJ(), testbit_J() 11 clearbit_l(), которые соответственно задают, проверяют и удаляют значение отдельного бита. Функции setbit_J() и clearbit_l() возвра- щают состояние указанного бита до выполнения операции. Пози- ции битов отсчитываются от 0, и заданную позицию можно пред-
рддВА 7. Поразрядные и логические функции 157 ставить как логарифм степени двойки: если nJ равно 0, то setbitj (nJ, 0) возвращает значение 0 и после выполнения операции nJ о имеет значение 2=1. После обращения к функции setbitJ(nJ, 512) 512 nJ принимает значение 2 . функция: Проверка и задание значения бита в CLINT-объекте Синтаксис: int setbitj (CLINT aj, unsigned int pos); Вход: a J (C LI NT-аргумент), pos (позиция бита, считая от 0) Выход: aJ (результат) Возврат: 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)) { ( | При необходимости a J заполняется нулями пословно, и новая А длина сохраняется в a J[OJ: for (i = DIGITSJ. (aj) + 1;i <= shorts + 1; i++) { aJ[i] = 0;
158 _____________________________________Криптография на Си и C++ в действии } SETDIGITS_L (a_l, shorts + 1); } Разряд числа а_1, содержащий позицию указанного бита, прове- ряется посредством наложения заранее заданной маски т, и t( / w после этого бит принимает единичное значение сложением OR соответствующего разряда с т. По окончании работы функция возврашает предыдущее значение бита. if (aj[shorts + 1] & m) ( i res = 1; aj[shorts + 1] |= m; return res; } Функция: Проверка двоичного разряда CLINT-объекта Синтаксис: int testbitj (CLINT aj, unsigned int pos); Вход: aj (CLINT-аргумент), pos (позиция бита, считая от 0) Выход: 1 - если значение бита в позиции pos задано О-в противном случае int testbitj (CLINT aj, unsigned int pos) -w { int res = 0; USHORT shorts = (USHORT)(pos » LDBITPERDGT); USHORT bitpos = (USHORT)(pos & (BITPERDGT - 1)); if (shorts < DIGITSJ_ (aj)) I { 1 if (aj[shorts + 1] & (USHORT)(1 U « bitpos)) 1 res = 1; 1 } 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 имеет достаточное количество разрядов, то разряд числа а_1, содержащий позицию указанного бита, проверяется посред- ством наложения заранее заданной маски m и бит принимает ну- левое значение с помощью операции AND соответствующего разряда с т. По окончании работы функция возвращает преды- дущее значение бита. Ш** 4 if (a_l[shorts + 1] & m) { res = 1; } ajfshorts + 1] &= (USHORT)(~m); RMLDZRSJ. (aJ); } return res;
160 Криптография на Си и C++ в действии 7.4. Операции сравнения В каждой программе приходится проверять условия равенст* ва/неравенства или соотношения величин арифметических пере* менных. Это требование справедливо и при работе с CLINT* объектами. Здесь мы тоже исходим из того, что программист не обязан знать внутреннюю структуру СLINT-типа, и то, как соотно- сятся друг с другом два CLINT-объекта, определяют специально разработанные для этих целей функции. Основная функция, осуществляющая сравнение, - это функция cmp_l(). Она определяет, какое из соотношений выполняется для двух CLINT-величин a.I и b.l - a.I < b_l, a_l == b.l или a_l > b_l. С этой целью сначала сравнивается количество разрядов CLINT- объектов, из которых предварительно исключаются ведущие нули. Если количество разрядов операндов одинаково, тогда работа на- чинается со сравнения старших разрядов. Как только обнаружива- ется несовпадение, сравнение заканчивается. Функция: Синтаксис: Вход: Возврат: Сравнение двух CLINT-объектов int cmpj (CLINT aj, CLINT b.l); a J, bj (аргументы) -1 - если значение aj < значения bj О - если значение a.I = значению b.l 1 - если значение а_1 > значения bJ int cmpj (CLINT a.I, CLINT b.l) { clint *msdptra_l, *msdptrb_l; int la = DIGITS.L (a.I); int lb = DIGITS.L (b.l); Первый тест проверяет, не равны ли нулю длины (а, следовательно, и значения) обоих аргументов. Затем исключаются ведущие нули и делается попытка принять решение, исходя из числа разрядов: if (la == 0 && lb == 0) { return 0;
7. Поразрядные и логические функции 161 while (а_1[1а] == 0 && 1а > 0) { --1а; } while (b J[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 > aj)) { msdptraj-; msdptrbj-;
162 Криптография на Си и C++ в действии Теперь сравниваем эти два разряда и делаем вывод. Функция при этом возврашает соответствующее значение: if (msdptraj == a_l) return 0; if (*msdptraj > *msdptrbj) return 1; else return -1; Если нас интересует равенство двух CLINT-величин, то применение функции cmpj() влечет за собой выполнение излишних операций. Для этого случая имеется более простой ее вариант, где отсутствует сравнение размеров. Функция: Сравнение двух CLINT-объектов Синтаксис: int equj (CLINT aj, CLINT bj); Вход: a J, bj (аргументы) Возврат: 0 - если значение aj значению bj 1— если значение aj = значению bj int equj (CLINT aj, CLINT bj) { clint *msdptraj, *msdptrbj; int la = DIGITSJ. (aj); int lb = DIGITSJ- (bj); if (la == 0 && lb == 0)
j-дДВА 7. Поразрядные и логические функции 1 53 { return 1; } while (aJ[la] == 0 && la > 0) J г „ { --la; ?т"'Ч j ' ‘"6 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++ в действий ----------------------------------------------------------- Применение пользователем этих двух функций в непосредственном виде легко приведет к многочисленным ошибкам. В частности, смысл результатов функции cmpj() необходимо твердо запомнить, или же придется их периодически освежать в памяти. В качестве средства против ошибок было разработано множество макросов, с помощью которых можно представить условия сравнения в более удобном виде. (см. Приложение С, «Макросы с параметрами»). Например, есть следующие макросы, в которых объекты а_1 и bj приравниваются к их величинам: GE_L (a_l, b J) EQZ_L (a J) возвращает 1, если а_1 >= Ь, и 0 - в противном случае; возвращает 1, если а_1 == 0, и 0, если а_1 > 0.
f Л AB A 8. Операции ввода, вывода, присваивания и преобразования Теперь числа из двоичной системы в десятич- ную преобразовывались автоматически... 881, 883, 887, 907... знакомые простые числа. Карл Саган, Контакт В начале этой главы рассмотрим самую простую и одновременно самую важную функцию - присваивание. Для того чтобы присвоить CLINT-объекту а_1 значение другого CLINT-объекта Ь_1, нам нужна функция, которая копирует разряды Ь_1 в область памяти, отведен- ную под а_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) { clint * *lastsrcj = MSDPTFLL (srcj); *destj = *srcj; На следующем шаге находятся ведушие нули, которые потом от- брасываются. Одновременно устанавливается количество разря- дов в выходном объекте.
Криптография на Си и C++ в действии 166 while ((*lastsrcj » 0) && (*destj > 0)) -lastsrcj; -*dest_l; Теперь значимые разряды исходного объекта копируются в объ- ект назначения. После этого функция заканчивает работу: while (srcj < lastsrcj) { *++destJ = *++srcJ; } - ' ; ) / < Обмен значениями двух CLINT-объектов осуществляется с помо- щью макроса SWAP_L (FLINT/C-варианта макроса SWAP), который весьма интересным способом выполняет эту задачу посредством операций XOR, не вводя временные переменные для промежуточ- ного хранения: #define SWAP(a, b) ((а)Л=(Ь), (Ь)Л=(а), (а)Л=(Ь)) #define SWAP J_(aJ, bJ) \ (xorj((aj), (bj), (aj)),\ xorj((bj), (aj), (bj)),\ xorj((aj), (bj), (aj))) Недостатком этих макросов является то, что если их входные аргу- менты являются некоторыми выражениями, то могут возникать побочные эффекты их повторного вычисления и отсюда труднооб- наружимые ошибки. Для SWAP_L это не так критично, так как эта функция может работать только с указателями на CLINT-объекты, и любое выражение при вызове этой функции все равно должно быть преобразовано в такой указатель. В необходимых случаях вместо макроса можно использовать функцию fswapJQ. Функция: Перестановка значений двух CLINT-объектов Синтаксис: void fswapj (CLINT aj, CLINT bj); Вход: aj, bj (переставляемые значения) Выход: a J, bj
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 167 Хотя функции библиотеки FLINT/C для ввода и вывода чисел в дос- тупном для понимания виде не принадлежат к числу поражающих воображение, все же во многих приложениях без них не обойтись. 1 Из практических соображений вид этих функций выбирался таким 5 образом, чтобы ввод и вывод осуществлялся в виде строк символов i (векторов типа char). Для этого были разработаны две по существу । дополняющие друг друга функции str2clint_J() и xclint2str_1(): первая преобразует строку цифр в CLINT-объект, а вторая, наоборот, пре- образует CLINT-объект в строку. При этом задается основание для представления строк, причем допускаются представления по осно- ванию в интервале от 2 до 16. Функция str2clintj() осуществляет преобразование представления в объект типа CLINT из представления по заданному основанию. Такое преобразование выполняется посредством последовательных умножений и сложений по основанию В (см. [Knut], п. 4.4). Функ- ция отмечает все случаи переполнений, использования недопусти- мого основания и передачи пустых указателей и возвращает соот- ветствующие коды ошибок. Любые префиксы, характеризующие представление числа - «ОХ», «Ох», «ОВ» или «ОЬ» игнорируются: Функция: Преобразование строки в CLINT-объект Синтаксис: int str2clintJ (CLINT nJ, char *str, USHORT base); Вход: str (указатель на последовательность типа char) base (основание числового представления строки, 2 < base < 16) Выход: nJ (выходной CLINT-объект) Возврат: E_CLINT_OK, если все в порядке E_CLINT_BOR, если base < 2 или base > 16, или в str есть разряды, большие, чем base E_CLINT_OFL в случае переполнения E_CLINT_NPT, если в str передан пустой указатель int str2clintj (CLINT nJ, char *str, USHORT base) { clint baseJ[10]; y { . . J USHORT n; / int error = E_CLINT_OK; • | " J if (str == NULL) {
168 Криптография на Си и C++ в действии о return E_CLINT_NPT; ) if (2 > base || base > 16) { return E_CLINT_BOR; Г Ошибка: недопустимое основание */ } u2clint_l (base_l, base); SETZERO_L (nJ); if (*str == ’0‘) r . «о ' t if ((tolowerj(‘(str+1)) == ’x') || (tolowerj(*(str+1)) == *b’)) !* Игнорировать любой префикс */ { ++str; ++str; } : Rqo.q 4 i . } while (isxdigit ((int)*str) || isspace ((int)*str)) r t if (iisspace ((int)*str)) { n = (USHORT)tolowerJ (*str); Многие реализации функции tolowerO из С-библиотек, несовмес- Q wr тимых с ANSI, возврашают неопределенные результаты, если символ не является заглавным. FLINT/C-функция tolowerJO об- ращается к tolowerO только в случае заглавных A-Z, в противном случае возвращает символ неизмененным.
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 169 switch (n) { case 'a': . i •Г J case *b‘: case 'c': case 'd‘: case ‘e’: case T: n -= (USHORT)Ca’ - 10); break; default: n -= (USHORT)*0'; } "*• - if (n >= base) { error = E_CLINT_BOR; break; } V if ((error = mulj (nJ, basej, nJ)) != E_CLINT_OK) { break; } if ((error = uaddj (nJ, n, nJ)) != E_CLINT_OK) { break; .Г } } ++str; } return error;
170 Криптография на Си и C++ в действии Функция xclint2strj(), обратная к функции str2clintj(), возвращает указатель на внутренний буфер класса памяти static (см. [Harb], п. 4.3), который сохраняет полученное численное представление и его значение до следующего вызова функции xclint2str() или до окончания работы программы. Функция xclint2strj() выполняет требуемое преобразование CLINT- представления в представление по заданному основанию путем последовательных делений с остатком на основание В. Функция: Преобразование CLINT-объекта в строку символов Синтаксис: char * xclint2strj (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] = {Ю717273,,,47576,,7,,,8,,'9,,,а,,,Ь,,,с*,,с1',‘е,,Т}; char * \ xclint2strj (CLINT nJ, USHORT base, int showbase) { CLINTD uj, rj; 1 clint baseJ[10]; | int i = 0; static char N[CLINTMAXBIT + 3]; if (2U > base || base > 16U) { return (char *)NULL; /* Ошибка: недопустимое основание 7 u2clintj (basej, base); cpyj (uj, nJ);
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 171 do { (void) div_l (uj, basej, uj, r_l); if (GTZ_L (r_l)) N[i++] = (char) ntable[‘LSDPTR_L (r_l) & OxffJ; } else "" { N[i++] = 'O'; } } while (GTZ_L (uj)); if (showbase) { switch (base) { case2: N[i-+-h] ='b'; N[i++] = ,°’; break; v.,. case 8. N[i++] = '0'; t break; case 16: N[i++] = *x*; N[i++] = '0'; break; } N[i] = '\0';
172 Криптография на Си и C++ в действии return strrevj (N); II } Для совместимости с функцией clint2strj() в первом издании книги clint2str_l (nJ, 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- объекта (см. приложение С). В случае преобразования друг в друга байтовых векторов и СLINT- объектов можно применить пару функций byte2clintj() и clint2byte_l() (см. [IEEE], 5.5.1). Предполагается, что байтовые вектора осуще- ствляют численное представление по основанию 256 с возрастаю- щими справа налево значениями. Подробную реализацию этих функций читатель найдет в файле flint.c. Здесь мы приводим только их заголовки. Функция: Преобразование байтового вектора в CLINT-объект Синтаксис: int byte2clintJ (CLINT nJ, UCHAR *bytestr, int len); Вход: bytestr (указатель на последовательность типа UCHAR) len (длина байтового вектора) Выход: nJ (выходной CLINT-объект) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения E_CLINT__NPT, если в bytestr был передан пустой указатель
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 173 функция: Преобразование CLINT-объекта в байтовый вектор Синтаксис: UCHAR * clint2bytej (CLINT nJ, int *len); Вход: nJ (преобразуемый CLINT-объект) Выход: len (длина генерируемого байтового вектора) Возврат: указатель на полученный байтовый вектор NULL, если в len передан пустой указатель И наконец, для перевода значений типа unsigned в численный фор- мат CLINT можно использовать две функции: u2clint_l() и ul2clintj(). Функция u2clintj() преобразует аргументы типа USHORT (а функция ul2clintj() - аргументы типа ULONG) в численный формат CLINT. Например, функция u2clintj() в дальнейшем будет описываться следующим образом: Функция: Преобразование значения типа ULONG в CLINT-объект Синтаксис: void ul2clintJ (CLINT numj, ULONG ul); Вход: ul (преобразуемое значение) Выход: numj (выходной CLINT-объект) void ul2clintj (CLINT numj, ULONG ul) { *LSDPTR_L (numJ) = (USHORT)(ul & Oxffff); *(LSDPTR_L (numJ) + 1) = (USHORT)((ul » 16) & Oxffff); SETDIGITS J. (numj, 2); RMLDZRSJ. (numj); } Завершая эту главу, рассмотрим функцию, которая выполняет про- верку правильности числового формата CLINT-объекта. Контроль- ные функции этого типа вызываются всякий раз, когда в систему вводятся «чужеродные» величины для дальнейшей обработки в под- системе. Такой подсистемой может оказаться, например, какой- либо криптографический модуль, который перед каждой обработ- кой входных данных должен проверить их допустимость. Динамиче- ская проверка входных значений функции весьма полезна в практике программирования, так как она позволяет избегать неопределённых ситуаций и имеет решающее значение для стабильной работы при- ложений. При тестировании и отладке эта проверка обычно проис- ходит с помощью утверждений (assertions), проверяющих условия
174 Криптография на Си и C++ в действии во время выполнения. Утверждения вставляются в программу как макросы, и при фактическом выполнении программы, обычно во время компиляции, их можно отключить посредством #define NDEBUG. В дополнение к макросу assert стандартной библиотеки языка С (см. [Plal], глава 1) имеется ещё несколько подобных реа- лизаций, которые выполняют различные действия в случае жестких условий тестирования, например, такие как распечатка обнаружен- ных исключительных ситуаций в файл регистрации, с завершением (или без завершения) работы программы при появлении ошибки. Подробную информацию по этому вопросу читатель может найти в [Magu], главы 2 и 3, а также в [Murp], глава 4. • мик»м*мм>:>. »s ыамг i Защита функций из библиотеки программ (такой как пакет FLINT/C) от передачи значений, не входящих в область определения соответ- ствующих параметров, может происходить внутри или самих вызы- ваемых функций, или функций, их вызывающих. В последнем слу- чае вся ответственность возлагается на программиста, пользующе- гося этой библиотекой. Исходя из соображений эффективности, при разработке FLINT/C-функций мы не проверяли каждый пере- даваемый CLINT-аргумент на правильность адреса и на возмож- ность переполнения. Представив многократные проверки формата числа для тысяч операций модульного умножения или возведения в степень, автор решил переложить эту задачу на сами программы, использующие функции из FL1NT/C. Исключением является пере- дача делителей с нулевым значением. Здесь проверка имеет прин- ципиальное значение, и обнаружение нуля подтверждается сооб- щением о соответствующей ошибке во всех функциях, реализую- щих арифметику классов вычетов. Текст всех этих функций прове- рялся особенно тщательно, чтобы удостовериться, что библиотека FLINT/C генерирует только допустимые форматы (см. главу 12). Для проверки правильности формата CLINT-аргументов была раз- работана функция vcheckj(). Она предназначена для защиты FLINT/C-функций от передачи недопустимых параметров в качестве CLINT-величин. Функция: Проверка правильности числового формата CLINT-объекта Синтаксис: int vcheckj (CLINT nJ); Вход: nJ (проверяемый объект) Возврат: E_VCHECK_OK - если формат правильный сообщение об ошибке в соответствии с таблицей 8.1 int vcheckj (CLINT nJ) unsigned int error = E_VCHECK_OK;
ГЛАВА 8. Операции ввода, вывода, присваивания и преобразования 175 Проверяем наличие нулевого указателя: самая ужасная из ошибок. if (nJ == NULL) { error = E_VCHECK_MEM; } else { Проверяем наличие переполнения: не слишком ли много у числа разрядов? if (((unsigned int) DIGITSJ- (nJ)) > CLINTMAXDIGIT) { error = E_VCHECK_OFL; } else { ---------------------------------------------------------1 Проверяем наличие ведущих нулей: с ними можно жить. | if ((DIGITSJ. (nJ) > 0) && (nJ[DIGITS_L (nJ)] == 0)) { error = E_VCHECK_LDZ; } } return error;
176 Криптография на Си и C++ в действии Возвращаемые функцией значения описаны как макросы в файле flint.c. Их объяснение приводится в таблице 8.1. Таблииа 8.1. Сообшения функиии vc heckJO о результатах проверки Возврашаемое значение Сообщение Интерпретация E_VCHECK_OK Format is OK Число имеет допустимое представление, и его значение лежит в пределах CLINT-типа. E_VCHECK_LDZ leading zeros Предупреждение: число обладает ведущими нулями, однако его величина лежит в допустимых пределах. -м. . E_VCHECK_MEM memory error Ошибка: передан нулевой указатель ъ --h ' E_VCHECK_OFL genuine overflow Ошибка: переданное число слишком большое; его нельзя представить как CLINT-объект. Численные значения кодов ошибок меньше нуля, поэтому доста- точно простого сравнения с нулем, чтобы отличить ошибки от пре- дупреждений (или от допустимого случая). .4ТНЖ ste /ллшкж» <, -аг
рЛАВА 9. Динамические регистры - От глупости этой машины я впадаю в депрес- сию, - сказал Марвин и поковылял прочь. Дуглас Адамс, Ресторан на краю Вселенной. Кроме автоматических или, в исключительных случаях, глобаль- ных CLINT-объектов, используемых до сих пор, иногда оказывается полезным умение создавать и уничтожать CLINT-переменные авто- матически. С этой целью разработаем несколько функций, которые позволят нам генерировать, использовать, удалять и перемещать совокупность CLINT-объектов - так называемый банк регистров - как динамически распределенную структуру данных. Мы восполь- зуемся схемой, представленной в [Skal], и приспособим её для ра- боты с CLINT-объектами. Будем разделять эти функции на закрытые функции управления и открытые функции. Последние будут доступны другим внешним функциям для работы с регистрами. Однако сами CLINT-функции не используют эти регистры, так что полный контроль над исполь- зованием регистров могут обеспечить функции пользователя. При выполнении программы должно быть установлено число дос- тупных регистров. Для этого нам требуется статическая переменная NoofRegs, принимающая значение числа регистров, которое опре- деляется встроенной константой NOOFREGS. static USHORT NoofRegs = NOOFREGS; Теперь определим центральную структуру данных для управления банком регистров: struct clint_registers { int noofregs; int created; clint **reg_l; /* указатель на вектор CLINT-адресов */ Структура clint_registers содержит: переменную noofregs, которая описывает число регистров, помещенных в наш банк; переменную created, которая указывает, выделена ли эта совокупность регист- ров; указатель reg_l на вектор, содержащий начальные адреса от- дельных регистров. static struct clint_registers registers = {0, 0, 0};
178 Криптография на Си и C++ в действии Теперь рассмотрим закрытые функции управления: allocate_regj() чтобы создать банк регистров, и destroy_reg J() - чтобы уничтожить его. Сначала создается область хранения адресов выделяемых реги- стров и устанавливается указатель на переменную registers.regj. После этого выделяется память для каждого отдельного регистра посредством вызова malloc() из стандартной библиотеки языка С. Тот факт, что CLINT-регистры являются единицами памяти, выде- ** ' ленными с помощью malloc(), играет важную роль при тестирова- нии FLINT/C-функций. В п. 12.2 мы увидим, что благодаря этому можно проверять память на наличие любых возможных ошибок, я 7 static int allocate_regj (void) Я { w USHORT i, j; Сначала выделяем память для вектора с адресами регистров. if ((registers.regj = (clint **) malloc (sizeof(clint *) * < NoofRegs)) == NULL) return E_CLINT_MAL; } Теперь выделяем отдельные регистры. Если в процессе работы вызов mallocO завершится ошибкой, все регистры, выделенные до этого, очишаются и возврашается код ошибки E_CLINT_MAL: for (i = 0; i < NoofRegs; i++) { if ((registers.reg J[i] = (clint *) malloc (CLINTMAXBYTE)) == NULL) { for (j = 0; j < i; j++) { free (registers.reg_J[j]); } return E__CLINT_MAL; /* ошибка: malloc */ }
fдАВА 9. Динамические регистры 179 } return E_CLINT_OK; ) Функция destroy_reg_l() по существу является противоположной функции create_regj(). Сначала обнуляется содержимое регистров. Затем память, выделенная под каждый регистр, возвращается с по- мощью free(). Наконец, освобождается область памяти, на которую указывал registers.regj. static void u destroy_regj (void) • 1? > ( Г USHORT i; 4 - г- for (i = 0; i < registers.noofregs; i++) ;/ { memset (registers.reg_l[i], 0, CLINTMAXBYTE); wr free (registers.regJ[i]); TC'{ } 4 и free (registers.regj); ’.30’1 Теперь рассмотрим общие функции для управления регистрами. С помощью функции 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_regj(), описанной ниже, получа- ем совокупность регистров, которые освобождаются только в том случае, если значение registers.created равно 1. В противном случае registers.created просто уменьшается на 1. Используя этот меха- низм, называемый семафором, мы предотвращаем ситуацию, когда совокупность регистров, выделенных одной функцией, может быть без всякого указания освобождена другой функцией. С другой сто- роны, каждая функция, которая запрашивает совокупность регист- ров, вызывая create_reg_l(), должна освобождать их посредством опять же free_reg_l(). Более того, нельзя допускать, чтобы после вы- зова функции регистры содержали конкретные значения. Переменную NoofRegs, определяющую число регистров, создаваемых функцией create_regj(), можно изменять посредством функции set_noofregs_J(). Однако это изменение остаётся действительным только до тех пор, пока не будет освобождена текущая выделенная совокупность регистров и создана новая с помощью create_reg_J(). Функция: Задание числа регистров Синтаксис: void set_noofregsj (unsigned int nregs); Вход: nregs (число регистров в банке регистров) void set_noofregsj (unsigned int nregs)
ГДАВЙЙ- Динамические регистры____________________________________________181 NoofRegs = (USHORT)nregs; ) Теперь, когда мы умеем выделять совокупность регистров, можно задаться вопросом, как получить доступ к отдельным регистрам. Для этого необходимо выбрать динамически выделенное функцией create_regj() поле адреса regj в описанной выше структуре clint_registers. Эта задача выполняется посредством функции get_regj(), представленной ниже, которая возвращает указатель на отдельный регистр из совокупности, при условии, что выделенный регистр обозначается определенным порядковым числом. функция: Вывод указателя на регистр Синтаксис: clint * get_reg_l (unsigned int reg); Вход: reg (номер регистра) Возврат: указатель на требуемый регистр reg, если он выделен NULL, если регистр не выделен clint * get_reg_l (unsigned int reg) / •j'-h (j 1 if (’.registers.created || (reg >= registers.noofregs)) f t return (clint *)NULL; } return registers.regj[reg]; } Так как размер совокупности регистров и её расположение в па- мяти может измениться, то не рекомендуется сохранять уже счи- танные адреса регистров для использования их в дальнейшем. Намного предпочтительнее каждый раз заново получать адреса регистров. В файле flint.c можно найти несколько встроенных макросов вида #define rOJ get_reg_l(O); С помощью этих макросов можно вызывать регистры по их факти- ческим текущим адресам, не прибегая к дополнениям в тексте про- граммы. Используя функцию purge_reg_l(), можно очистить от- дельный регистр совокупности путём затирания его содержимого.
182 Криптография на Си и C++ в действии ” . функция: Очистка CLINT-регистра из банка регистров с помощью заполнения его нулями Синтаксис: int purge_reg_l (unsigned int reg); j j Вход: 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.regj[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.regj[i], 0, CLINTMAXBYTE);
ГЛАВА 9. Динамические регистры 183 f?*- return E_CLINT_OK; } return E_CLINT_NOR; } В программировании считается хорошим стилем и проявлением профессиональной этики освобождать выделенную память, когда она больше не нужна. Имеющуюся в наличии совокупность регист- ров можно освободить с помощью функции free_reg_l(). Однако, как мы уже объясняли выше, перед освобождением выделенной памяти семафор registers.created в структуре registers должен быть установлен в 1: ’94 < , t Г! Г ’ ' ’JKi .h* void free_regj (void) { if (registers.created == 1) { destroy_regj (); ' П.00 07 .nv он ж ' r!TR‘ if (registers.created) { -registers.created; П } Теперь рассмотрим три функции, которые создают, очищают и ос- вобождают отдельные CLINT-регистры, по аналогии с управлением всей совокупностью регистров. Функция: Выделение регистра CLINT-типа Синтаксис: clint * createj (void); Возврат: указатель на выделенный регистр, если все в порядке NULL в случае ошибки, связанной с malloc() clint * createj (void) {
184 Криптография на Си и С+4* в действии return (clint *) malloc (CLINTMAXBYTE); } При этом важно не «потерять» возвращаемый функцией createJQ указатель, так как иначе будет невозможно получить доступ к соз- данному регистру. Последовательность clint * do_not_overwritej; clint * lost I; 1 do_not_overwritej = createJO; /* ... */ do_not_overwritej = lost J; выделяет регистр и хранит его адрес в переменной с соответст- вующим именем do_not_overwritej {не затирать). Если эта пере- менная содержит только ссылку на регистр, то после выполнения команды do_not_overwritej = lost J; регистр оказывается висячим (или потерянным, на него больше ничто не ссылается). Это типичная ошибка, которую можно допус- тить, блуждая «в дебрях» управления динамической памятью. Регистр, как и любую другую CLINT-переменную, можно очистить с помощью представленной ниже функции purgeJO, посредством которой зарезервированная для указанного регистра память запол- няется нулями и таким образом очищается. Функция: Очистка CLINT-объекта с помощью заполнения нулями Синтаксис: void purgej (CLINT nJ); Вход: nJ (CLINT-объект) void purgej (CLINT nJ) { if (NULL != nJ) { memset (nJ, 0, CLINTMAXBYTE);
|-дДВА 9. Динамические регистры 185 Следующая функция после очистки регистра к тому же освобождает выделенную для него память. После этого к регистру больше нельзя получить доступ. функция: Очистка и освобождение CLINT-регистра Синтаксис: void free_l (CLINT reg J); Вход: regj (указатель на CLINT-регистр) void freej (CLINT reg_l) { if (NULL != regj) { memset (regj, 0, CLINTMAXBYTE); free (n_l); }
fjjABA 10. Основные теоретико-числовые функции Я ужасно хочу это услышать, поскольку я всегда считал теорию чисел Королевой Математики - самым чистым направлением математики - единственным направлением, НЕ имеющим прикладного значения! Д.Р. Хофштадтер, Гедель, Эшер, Бах Вооруженные целым арсеналом арифметических функций, разра- ч ботанных в предыдущих главах, обратимся теперь к реализации не- : ,J которых фундаментальных алгоритмов из теории чисел. Коллекция * теоретико-числовых функций, которые нам предстоит обсудить, с одной стороны, позволяет уяснить прикладные аспекты арифметики больших чисел, а с другой стороны, является этапом на пути к более ; сложным теоретико-числовым вычислениям и криптографическим *, приложениям. Рассматриваемые функции допускают сколь угодно широкое обобщение, так что из них можно «собрать» вычислитель- ный модуль почти для любого приложения. Алгоритмы, на основе которых написаны программы, приведенные в этой главе, взяты в основном из работ [Cohe], [HKW], [Knut], пг [Kran] и [Rose]. Как и раньше, нас особенно интересует эффек- та,. тивность и как можно более широкий спектр действия этих алгоритмов. * Что касается математической теории, то здесь мы приведем лишь тот минимум, который необходим для уяснения представленных функций и обоснования возможности их применения, - в конце концов, можно и отдохнуть. Те же, кто ожидал более радикального введения в теорию чисел, смогут найти его в книгах [Bund] и [Rose]. Алгоритмические аспекты теории чисел ясно и кратко из- ложены в работе [Cohe]. Информативный обзор теоретико- числовых приложений дан в [Schr], а криптографические аспекты теории чисел - в [КоЫ]. 4 В этой главе, помимо всего прочего, мы рассмотрим способы вы- числения наибольшего общего делителя и наименьшего общего кратного больших чисел, мультипликативные свойства кольца ! классов вычетов, научимся распознавать квадратичные вычеты и ‘ извлекать квадратные корни в кольцах классов вычетов, научимся ё применять китайскую теорему об остатках для решения систем ли- I нейных сравнений и тестировать числа на простоту. Теоретические ' сведения будут подкреплены практическими примерами. Кроме того, мы разработаем несколько функций, реализующих описанные алгоритмы, и укажем приложения, в которых эти функции могут быть полезны.
188 Криптография на Си и C++ в действии ----. 10.1. Наибольший обший делитель То, что школьников учат использовать для вы- числения наибольшего общего делителя двух целых чисел метод разложения на простые множители, а не более естественный способ - алгоритм Евклида, - позор для нашей системы образования. . >. У. Хейс, П. Кватроччи, Информация и теория кодирования, 19831 Наибольшим общим делителем (НОД) целых чисел а и b называ- ется положительный делитель чисел а и Ь, делящийся на любой другой общий делитель этих чисел. Таким образом, наибольший общий делитель определен однозначно. В математических обозна- чениях число d является наибольшим общим делителем двух не- нулевых целых чисел а и b, d = НОД(«, Z?), если d > 0, d\a, d\b и если для некоторого целого d' выполняются условия d' | а и d' | Ь, < * то d' | d. Это определение удобно дополнить, введя в рассмотрение случай НОД(0, 0) := 0. г.. Таким образом, мы определили наибольший общий делитель для всех пар целых чисел, в частности, для всех чисел, представимых CLINT-объектами. Выполняются следующие свойства: г - .d (а) НОД(а, Ь) = НОД(Ь, а), (10.1) (б) НОД(а, 0) = |а| (абсолютное значение числа а), (в) НОД(а, Ь, с) = НОД(л, НОД(й с)), (г) НОД(а, Ь) = НОД(-а, Ь), из которых лишь первые три применимы к CLINT-объектам. Сначала непременно следует рассмотреть классический алгоритм л ‘ ' вычисления наибольшего общего делителя, которым мы обязаны греческому математику Евклиду (III в. до н. э.) и который Д. Кнут уважительно называет «дедушкой» всех алгоритмов (см. [Knut], стр. 316 и далее). В основе алгоритма Евклида лежит повторное де- ление с остатком: вычисляется вычет a mod b, затем b mod (a mod Й и так далее, пока остаток не будет равен нулю. 1 Что удивительно, в России то же самое. - Прим, перев.
189 ГЛАВА 10» Основные теоретико-числовые функции Алгоритм Евклида вычисления НОД(а, Ь) для а, b > О А; 1. Если b = 0, то результат: а и алгоритм заканчивает работу. НН . *1 Г. fX t 2. Положить г <— a mod b, а <— b, b <— г и вернуться на шаг 1. Для натуральных чисел а2 процесс вычисления наибольшего общего делителя по алгоритму Евклида выглядит так: = «2^1 + а3, 0 < я3 < а2, а2 = а3$2 + а4> 0 < Лд < Я3, «з = a4q3 + а5, 0 < а5 < а4, . Y£r-- * Г.С —у , •= О"’ Cltn-2 ^m-\Qm-2 < ^/п-Ь ^т-\ ~ Результат: ЭР) ' ' Т НОД(яь а2) = ат. п.*'- В качестве примера вычислим НОД(723, 288): 'W С ТЛ 723 = 288- 2+ 147, 288 = 147- 1 + 141, 147 = 141 • 1+6, 141=6- 23 + 3, 6 = 3- 2. Результат: НОД(723, 288) = 3. Эта процедура хороша как для вычисления вручную, так и для про- граммной реализации. Соответствующая программа короткая и бы- страя; кроме того, при ее написании трудно ошибиться. Следующие свойства целых чисел и наибольшего общего делителя открывают - по крайней мере теоретически - новые возможности для улучшения программной реализации алгоритма: (а) если числа а и b четные, то НОД(а, Ь) = НОД(а/2, Ь/2) • 2, (Ю.2) (б) если а четное и b нечетное, то НОД(я, Ь) = НОД(а/2, Ь),
190 Криптография на Си и C++ в действии (в) НОД(а, Ь) = НОД(л - Ь, Ь), (г) если числа а и b нечетные, то а - b четное и |а - Ь\ < шах(а, Ъ). Преимущество следующего алгоритма, опирающегося на эти свой* ства, состоит в том, что в нем используются лишь операции срав- нения длин, вычитания и сдвига CLINT-объектов, не требующие особых временных затрат. Для их реализации у нас есть хорошие функции; кроме того, здесь не нужно делить. Бинарный алгоритм Евклида вычисления наибольшего общего делителя можно найти также в книгах [Knut] (п. 4.5.2, Алгоритм В) и [Cohe] (п. 1.3, Алго- ритм 1.3.5) почти в такой же форме. Бинарный алгоритм Евклида вычисления НОД(а, Ь) для а, Ъ > О 1. Если а < Ь, то поменять местами а и Ь. Если b = 0, то результат: а и алгоритм заканчивает работу. В противном случае положить к<г-0 и, пока числа а и b оба четные, полагать к<г-к+1, а <— л/2, b «— Z?/2. (Свойство (а) исчерпано; хотя бы одно из чи- сел а и b стало нечетным.) 2. Пока а четное, повторять а <— а/2, пока а не станет нечетным. Или, если b четное, повторять b <— /?/2, пока b не станет нечет- ным. (Свойство (б) исчерпано; числа а и b теперь оба нечетные.) 3. Положить t <г- (а - Z?)/2. Если t = 0, то результат: 2ка и завершить алгоритм. (Здесь мы использовали свойства (б), (в) и (г).) 4. Пока t четное, повторять t <—1/2, пока t не станет нечетным. Если t > 0, то положить а <— f; иначе положить b <----t. Вер- нуться на шаг 3. Этот алгоритм легко превращается в программу. Воспользуемся предложением Коха [Cohe] и выполним на шаге 1 дополнительное деление с остатком, положив г <— a mod b, а <— b и b <— г. Тем самым мы уравняли длины операндов а и Ь, поскольку различие в размерах могло бы неблагоприятно сказаться на времени работы программы. Функция: Наибольший общий делитель Синтаксис: void gcdj (CLINT aaj, CLINT bbj, CLINT ccj); Вход: aaj, bbj (операнды) Выход: ccj (наибольший общий делитель) void gcdj (CLINT aaj, CLINT bbj, CLINT ccj) {
10. Основные теоретико-числовые функции 191 CLINT aj, b_l, г J, tj; unsigned int k = 0; int sign_of_t; Шаг 1. Если аргументы не равны, то меньший аргумент записы- вается в b_l. Если значение b_l равно 0, то наибольшим обшим делителем будет а_1. if (LT_L (aaj, bbj)) { cpyj (aj, bbj); cpyj (bj, aaj); } else { cpyj (aj, aaj); cpyj (bj, bbj); if (EQZ_L (bj)) { cpyj (ccj, aj); return; Выполняем деление с остатком, «укорачивая» больший операнд aj. Затем исключаем из aj и bj степени двойки. (void) divj (aj, bj, tj, rj); cpyj (aj, bj); cpyj (bj, rj); if (EQZ_L (bj)) { cpyj (ccj, aj);
192 Криптография на Си и C++ в действии return; } while (ISEVENJ. (aJ) && ISEVENJ- (bj)) I ' SV.. • t ++k; „j..- • ’I--. shrj (aj); shrj (bj); } Шаг 2. while (ISEVENJ. (aj)) { shrj (aJ); } while (ISEVENJ. (b_l)) ( shrj (bj); } Шаг 3. Здесь мы сравниваем aj и Ь_1 и учитываем, что разность этих чисел может быть отрицательной. Абсолютную величину разности записываем в tj, а знак разности - в целочисленную переменную sign_ofJ. Как только tj == 0, алгоритм завершается. do { if (GE_L (aj, bj)) { subj (aj, bj, tj); sign_ofJ = 1; } else
193 рдАВА 10. Основные теоретико-числовые функции { subj (bj, a J, tj); sign_ofj = -1; if (EQZ_L (tJ)) { cpyj (ccj, aj); /* ccj <- a 7 shiftj (ccj, (long int) k);/* ccj <- ccj*2**k 7 return; } ( ] Шаг 4. В зависимости от знака переменной tj записываем ее либо \ в aj, либо в b_l. while (ISEVENJ- (tj)) { shrj (tj); } if (-1 == sign_of J) { cpyj (bj, tj); } else { cpyj (aj, tj); } } while (1); } Все используемые здесь операции линейно зависят от числа разря- дов операндов, однако тестирование показывает, что обычный ал- горитм Евклида из двух строк (см. стр. 189), реализованный в виде функции FLINT/C, значительно медленнее, чем только что рас- смотренный вариант. Это странное явление мы можем объяснить
194 Криптография на Си и C++ в действии ' - лишь тем, что, во-первых, наша программа деления не слишком эффективна, а во-вторых, последняя версия алгоритма имеет не- сколько более сложную структуру. Вычисление наибольшего общего делителя для большего числа ар- гументов можно осуществлять путем многократного применения функции gcdj(), так как, согласно свойству (10.1, (в)), общий слу- чай рекурсивно сводится к случаю двух аргументов: (10.3) НОД(пь .... пг) = НОД(пь НОД(п2,иг)). Используя наибольший общий делитель, определим наименьшее общее кратное (НОК) двух CLINT-объектов aj и b_l. Наименьшим общим кратным ненулевых целых чисел иь ..., пг называется наи- меньший элемент множества {т е BN+ | и, делит ш, где i = 1, ..., г}. Это множество непусто, поскольку содержит по крайней мере про- изведение П;=1И1 • Наименьшее общее кратное двух чисел a, b е 7L равно частному от деления абсолютной величины произведения на наибольший общий делитель: (10.4) НОК(я, Ь) • НОД(я, b) = \ab\. Воспользуемся этим соотношением для вычисления наименьшего общего кратного чисел aj и bj. Функция: Наименьшее общее кратное (НОК) Синтаксис: int IcmJ (CLINT aj, CLINT bj, CLINT cj); Вход: a J, bj (операнды) Выход: cj (наименьшее общее кратное) Возврат: E_CLINT_OK, если все в порядке E_CLINT_OFL в случае переполнения int lcm_l (CLINT aj, CLINT bj, CLINT c_l) { CLINT gjjunkj; if (EQZ_L (aj) || EQZ_L (b_l)) { SETZERO_L (cj); return E_CLINT_OK; }
|-дДВА 10. Основные теоретико-числовые функции 195 gcdj (aj, bj, g_l); divj (aj, gj, gj, junkj); return (mulj (gj, bj, cj)); } * 1 , ; СУ"' Случай вычисления наименьшего общего кратного для большего числа аргументов также можно рекурсивно свести к случаю двух аргументов: (10.5) НОК(пь .... пг) = НОК(пь НОК(и2,пг)). с'. П но формула (10.4) не может быть расширена на большее, чем 2, число аргументов, как видно из простейшего примера: Г. НОК(2, 2, 2) • НОД(2, 2, 2) = 4 Ф 23. Тем не менее, соотношение ме- жду наибольшим общим делителем и наименьшим общим кратным можно обобщить на случай большего числа аргументов: (10.6) НОК(а, Ь, с) НОД(аЬ, ас, be) = |aZ><?| И (10.7) НОД(а, Ь, с) • НОКЖ ас, be) = \abc\ Интересная связь между наибольшим общим делителем и наи- меньшим общим кратным проявляется в следующих формулах, отражающих двойственность этих функций в том смысле, что, если поменять наибольший общий делитель и наименьшее общее крат- ное местами, формулы по-прежнему будут верны, как в случаях (10.6) и (10.7). Справедлив дистрибутивный закон: (10.8) НОД(а, НОЖ с)) = НОК(НОД(я, Ь), НОД(я, с)), (10.9) НОЖ НОД(6, с)) = НОД(НОЖ Ь), НОЖ с))> и, в довершение всего (см. [Schr], п. 2.4), (10.10) НОЖ НОД(6, с)) = НОД(НОЖ Ь), НОЖ с)), Эти формулы не только завораживают своей симметрией, но и пре- красно подходят для тестирования функций, касающихся наи- большего общего делителя и наименьшего общего кратного, когда неявно тестируются и арифметические функции (о тестировании см. главу 12). Не вините тестировщиков в том, что они нахо- дят ваши ошибки. Стив Мэгью
196 Криптография на Си и C++ в действии ...................1 ... .. 1 11 .................. 10.2. Обращение в кольце классов вычетов В отличие от множества целых чисел, в кольце классов вычетов при определенных условиях можно находить мультипликативно обратные элементы. Точнее, в кольце для элемента а е (во- обще говоря, не для каждого) существует элемент xg такой, что а • х = 1. Равенство для классов вычетов эквивалентно сравне- нию а • х = 1 mod п или, иначе, равенству а • х mod п = 1. Например, в кольце Zu элементы 3 и 5 мультипликативно обратны друг другу, поскольку 15 mod 14 = 1. Существование мультипликативно обратных элементов в кольце не очевидно. В главе 5 (стр. 84) мы определили, что (Z„, •) является лишь конечной коммутативной полугруппой с единицей 1. Доста- точное условие того, чтобы для элемента ае 71п существовал мультипликативно обратный, можно вывести из алгоритма Евклида. Преобразуем предпоследнее равенство алгоритма (см. стр. 189) ^т-2 ~ 0 < Clm < ат_\, II к виду ^т ~ & т-2 ^m-\Qm-2' (1) Продолжая в том же духе, получаем последовательно О-т-Х ~ & т-3 ~~ ^т-2^т-3^ (2) Я т-2 = &т-4 ~ (3) а3 = ах -a2qi. (in -2) Подставим в формулу (1) выражение для ат_\ из правой части фор- мулы (2): @т ~ & т-2 ~ Ят-1(^т-3 ~ Ят-3^т-2) ИЛИ &т = (1 + Ят-ЗЯт-2)^т-2 ~ Ят-2^т-3- Продолжив выкладки, получим в формуле (ш-2) представление числа ат в виде линейной комбинации начальных значений а\ и а* коэффициентами при которых будут неполные частные qt из алго- ритма Евклида. Таким образом, получаем представление наибольшего общего де- лителя g = НОД(я, Ь) = и- а + у- Ьв виде линейной комбинации чи- сел а и b с целыми коэффициентами миг, причем и по модулю alg и v по модулю b/g определены однозначно. Если же для элемента a G 7Ln выполняется условие НОД(а, п) = 1 = и • а + v • п, то отсюда
рдДВА 10. Основные теоретико-числовые функции 197 сразу же следует 1 = и • a mod п или, что то же самое, а • и = 1. В этом случае вычет и mod п определен однозначно и, следова- тельно, и является обратным к а в кольце Попутно мы нашли условие существования и вывели процедуру вычисления мультип- ликативно обратного к элементу кольца 2Л. Рассмотрим пример. Из проведенных ранее вычислений наибольшего общего делителя НОД(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. Быстрая процедура поиска линейного представления наибольшего общего делителя подразумевает вычисление и запоминание непол- < ( ных частных (что мы только что и сделали) и восстановление с их п помощью коэффициентов линейного представления. Эта процедура ., r v требует много памяти, а значит, непрактична. И вот перед нами ти- ч г, личная задача, возникающая при разработке и реализации алгорит- м мов: найти компромисс между требуемым временем вычисления и объемом памяти. Для начала нам нужно так изменить алгоритм Евк- лида, чтобы вычислять наибольший общий делитель и коэффици- енты линейного представления одновременно. Как мы видели, для a G 72-п существует обратный элемент хе если НОД(я, п) = 1. Верно и обратное утверждение: если для элемента ае Д сущест- 1 т вует мультипликативно обратный, то НОД(л, и) = 1 (строгое дока- 1 г зательство этого утверждения см. в работе [Nive], доказательство н теоремы 2.13). Отсюда становится понятно, почему так важен во- прос об отсутствии общих делителей (о взаимной простоте) чисел: подмножество 2Л* := { а е 2Л | НОД(я, п) = 1} кольца 2Л, состоящее из элементов a g взаимно простых с и, с определенной на нем операцией умножения является абелевой группой. В главе 5 мы обозначили эту группу через (Д,*, •)• На группу (£л\ •) переносятся все свойства абелевой полугруппы (^л, •) с единицей: ✓ ассоциативность, ✓ коммутативность, ✓ существование единицы: для любого a G выполняется а • 1 = а.
198 Криптография на Си и C++ в действиц Условие существования мультипликативно обратного также вы- полняется для всех элементов, поскольку именно такие элементы мы и выбирали. Так что теперь нам осталось проверить лишь замк- нутость по умножению, то есть что для любых элементов a, b кольца 7Ln произведение а • Ъ тоже будет элементом кольца Замкнутость доказывается легко: если элементы аиЬ взаимно про- сты с и, то произведение ab не может иметь нетривиального общего делителя с числом п, то есть произведение а • b должно принад- лежать множеству 7L* • Группа (Z„*, •) называется группой классов вычетов, взаимно простых с л2.Число элементов группы или, что то же самое, число целых чисел из множества {1,2, ..., п - 1}, взаимно простых с н, определяется функцией Эйлера ф(л). Для чис- ла и, представленного в виде произведения п = ррру ...р? различ- ных простых чисел рь ..., ph где числа положительны, функция Эйлера вычисляется как Ф(п) = ПрГ1(р,-1) /=1 (см., например, [Nive], пп. 2.1 и 2.4). Отсюда, в частности, следует, s ж что если число р простое, то в группе ровно р - 1 элементов.3 Если НОД(д, /0=1, то согласно теореме Эйлера, которая, в свою оче- редь, является обобщением малой теоремы Ферма, дф(л) = 1 mod и, *’ так что, найдя значение дф(л)-1 mod п, тоже можно определить муль- типликативно обратное кд.4 Например, если п = р • q, где числа р и q простые, р* q и а е Zn*, то = 1 mod п и, следовательно, вычет mod п является мультипликативно обратным к а по модулю п. Однако для вычисления этим способом нужно даже в лучшем случае знать значение <ф(/г), кроме того, сложность модуль- ного возведения в степень равна <?(log3n). Мы воспользуемся более практичным алгоритмом, который, во- первых, имеет сложность <9(log2/i), а во-вторых, не требует знания функции Эйлера. Для этого объединим приведенную выше проце- дуру с алгоритмом Евклида. Рассмотрим две переменных и и v, для которых следующий инвариант д, = Ui • а + V/ • b, 11 2 Чаще эта группа называется мультипликативной группой кольца. - Прим, перев. 3 В этом случае Z/7 является полем, поскольку обе группы (Z/7, +) и (2Р*, •) = (Zp \ {0}, •) являются абелевыми (см. [Nive], п. 2.11). Конечные поля используются, например, в теории кодирования, а уж их роль в современной криптографии трудно переоценить. 4 Малая теорема Ферма утверждает, что если число р простое, то для любого целого а справедливо ар = a mod р. Если а не делится на р, то ар~х = 1 mod р (см. [Bund], глава 2, §3.3). Малая теорема Ферма и ее обобщение — теорема Эйлера — стоят в ряду наиболее важных теорем в теории чисел.
ГЛАВА 10. Основные теоретико-числовые функции 199 будет справедлив на каждом шаге процедуры, описанной на стр. 189, где ам = «/-i mod По завершении алгоритма этот инвариант и даст нам коэффициенты линейного представления наибольшего общего делителя чисел а и Ь. Эта процедура называется расширенным алгоритмом Евклида. Следующий алгоритм заимствован из книги [Cohe], п. 1.3, Алго- ритм 1.3.6. Переменная v присутствует в нем неявно и вычисляется лишь в конце алгоритма как v := (d - и • a)/b. Расширенный алгоритм Евклида вычисления НОД(а, Ь) и чисел и и v таких, что НОД(а, Ь) = и • а + v • b, для а, Ь > 0 1. Положить и <— 1, d <— а. Если b = 0, то положить у <— 0 и завер- шить алгоритм; иначе положить vj <— 0 и v3 <— b. 2. Вычислить q и Z3 такие, что d = q • v3 + t3 и t3 < v3, поделив d с ос- татком на v3. Положить t\ <— и - q • vb и <— vb d <— v3, Vi <— tx и v3 <—t3. 3. Если v3 = 0, то положить v <— (d - и • d)lb и завершить алгоритм; иначе вернуться на шаг 2. Построим функцию xgcdJO с использованием вспомогательных функций sadd() и ssub(), вычисляющих знаковое сложение и вычи- тание (в исключительных случаях). Каждая из этих функций пред- варительно определяет знак своего аргумента, а затем вызывает ба- зовые функции add() и sub() (см. главу 5), выполняющие, соответ- ственно, сложение и вычитание без учета переполнения или потери значащих разрядов. Кроме того, на основе функции деления divj(), определенной для натуральных чисел, создадим вспомогательную функцию smod() для вычисления вычета a mod b, где a, b 6 Z и b > 0. Эти вспомогательные функции еще пригодятся нам при по- строении функции chinremJO, реализующей китайскую теорему об остатках (см. п. 10.4.3). При возможном обобщении библиотеки FLINT/C на целые числа этими функциями можно воспользоваться для работы со знаковыми типами. Порядок использования функции xgcdJO следующий: если оба ар- гумента удовлетворяют условию а, Ь> Атах/2, то в значениях и и v, возвращаемых функцией xgcdJO, может возникнуть переполнение. Для разрешения подобных ситуаций следует запасти место для этих значений, которые в этом случае будут объявлены вызы- вающей программой как переменные типа CLINTD или CLINTQ (см. главу 2).
200 Криптография на Си и C++ в действии Функция: Синтаксис: Вход: Выход: Расширенный алгоритм Евклида вычисления линейного представ- ления НОД(а, Ь) = и • а + v • b для натуральных чисел а, b void xgcdj (CLINT aj, CLINT bj, CLINT gj, CLINT uj, int *sign_u, CLINT vj, int *sign_v); a J, bj (операнды) g_l (наибольший общий делитель чисел aj и bj) uj, vj (коэффициенты при a_J и bj в линейном представлении числа gj) *sign_u (знак коэффициента u J) *sign_v (знак коэффициента vj) void xgcdj (CLINT aj, CLINT bj, CLINT dj, CLINT uj, int *sign_u, - ‘ Ы f' . . CLINT vj, int *sign_v) { CLINT v1 J, v3J, t1 J, t3J, qj; CLINTD tmpj, tmpuj, tmpvj; int sign_v1, sign J1; Г") Шаг 1. Задание начальных значений. cpyj (d_l, а_1); cpyj (v3J, bj); if (EQZ_L (v3J)) { SETONEJ- (uj); SETZERO J- (vj); *sign_u = 1; *sign_v = 1; return; } SETONEJ- (tmpuj); *sign_u = 1;
10. Основные теоретико-числовые функции SETZERO J. (v1_l); sign_v1 = 1; 201 Шаг 2. Основной цикл; вычисление наибольшего обшего делителя и коэффициента и. while (GTZ_L (v3J)) { divj (dj, v3J, qj, t3J); mulj (v1 J, qj, qj); signjl = ssub (tmpuj, *sign_u, qj, sign_v1, t1 J); cpyj (tmpuj, v1 J); *sign_u = sign_v1 ; cpyj (dj, v3J); cpyj (v1 J, t1 J); sign_v1 = signjl; cpyj (v3J, t3J); } Шаг 3. Вычисление коэффициента v и завершение процедуры. mult (aj, 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 • b нам нужен только коэффициент и. Найдя для и соответствующий положительный представитель, мы избавимся от необходимости оперировать с от- рицательными числами. Следующий алгоритм представляет собой вариант предыдущего, учитывает наше замечание и полностью ис- ключает вычисление коэффициента v. Расширенный алгоритм Евклида вычисления НОД(а, Ь) и мультипликативно обратного к a mod п для а > 0, п > О 1. Положить и «— 1, g <— a, <— 0 и и v3 <— п. 2. Вычислить q и Г3 такие, что g = q • v3 + t3 и < v3, поделив d с ос- татком на v3. Положить t\ <— и - q • Vi mod и, и <— vb g <— v3, Vi <— ti и v3 <— Z3. 3. Если v3 = 0, то результат: g = НОД(я, b), элемент и мультиплика- тивно обратный к a mod п и алгоритм завершен. Иначе вернуться на шаг 2. Шаг Ц <— и - q • vj mod п гарантирует, что числа vi и и будут неот- рицательными. По окончании алгоритма получаем: и е {1, ..., п - 1}. Реализуем этот алгоритм в виде следующей функции. Функция: вычисление мультипликативно обратного в кольце ~ZLn Синтаксис: void invj invJ(CLINT aj, CLINT nJ, CLINT gj,CLINT ij); Вход: a J, nJ (операнды) Выход: gj (наибольший общий делитель чисел aj и nJ) ij (обратный элемент к aj mod nJ, если он существует) void invj (CLINT aj, CLINT nJ, CLINT gj, CLINT ij) { CLINT v1 J, v3J, t1 J, t3J, qj; Проверка операндов на равенство нулю. Если хотя бы один из операндов равен 0, то обратного элемента не существует, чего нельзя сказать о наибольшем обшем делителе (см. стр. 188). В этом случае результирующая переменная ij не определена и полагается равной нулю.
рдДВА 10. Основные теоретико-числовые функции 203 if (EQZ_L (aJ)) if (EQZ_L (nJ)) -ле-ч { SETZERO J. (gj); SETZERO J. (ij); return; } else { cpyj (gJ, nJ); SETZERO J. (ij); i. return; } } else { if (EQZ_L (nJ)) cpyj (gj, aj); pr SETZERO J. (i J); return; } Шаг 1. Задание начальных значений. cpyj (gj, aj); cpyj (v3J, nJ); SETZEROJ_ (v1 J); SETONEJ- (t1J); do {
204 Криптография на Си и C++ в действии Шаг 2. После деления осуществляем проверку в GTZ_L(t3j), что позволяет избежать лишнего вызова функций mmuljО и msubJO при последнем прохождении цикла. До окончания цикла пере- менной i J ничего не присваиваем. divj (gJ, v3J, qj, t3J); if (GTZ_L (t3J)) { mmulj (v1 J, qj, qj, nJ); msubj (t1 J, qj, qj, nJ); cpyj (t1 J, v1 J); cpyj (v1 J, qj); cpyj (gj, v3J); cpyj (v3J, t3J); } } while (GTZ_L (t3J)); Шаг 3. Выполняем последнее присваивание. В качестве наиболь- шего общего делителя берем значение переменной v3j, и если оно равно 1, то в качестве обратного к aj берем значение пере- менной v1 I. cpyj (gj, v3J); if (EQONEJ. (gj)) cpyj (ij, v1 J); ''***' else SETZEROJ- (ij);
ГЛАВА 10. Основные теоретико-числовые функции 205 10.3. Корни и логарифмы В этом параграфе мы научимся вычислять целую часть квадратного корня и логарифмы по основанию 2 для объектов типа CLINT. Сна- чала рассмотрим вторую из этих функций, а затем с ее помощью будем вычислять первую: для натурального числа а будем искать число е такое, что Т < а < 2e+I. Число е = |_Iog2 a J есть целая часть - логарифма числа а по основанию 2 и равно уменьшенному на 1 числу значащих битов числа а. Для этого используется функция ldj(), входящая в состав многих других функций пакета FLINT/C, которая игнорирует ведущие нули и считает только значащие дво- ичные разряды CLINT-объекта. функция: Число значащих двоичных разрядов CLINT-объекта Синтаксис: unsigned int IdJ (CLINT aj); Вход: aj (операнд) Выход: Число значащих двоичных разрядов числа aj unsigned int Ek IdJ (CLINT nJ) / i unsigned int 1; USHORT test; f "I .VI .4 ? М Шаг 1. Определяем число значащих разрядов в системе счисления | по основанию В. * пш- I = (unsigned int) DIGITS_L (nJ); while (n J[l] == 0 && I > 0) { -i; } if (I == 0) { return 0; }
206 Криптография на Си и C++ в действии Шаг 2. Определяем число значащих битов в старшем разряде. Константа BASEDIV2 - это число, имеющее единичный старший бит и остальные нули (то есть 2BITPERDGT1). test = nj[l]; I «= LDBITPERDGT; while ((test & BASEDIV2) == 0) test «= 1; -I; } return I; } Теперь перейдем к вычислению целой части квадратного корня из натурального числа. Воспользуемся классическим методом Ньюто- на (называемым еще методом Ньютона-Рафсона), который обычно применяется для определения нулей функции путем последова- тельных приближений. Пусть функция Дх) дважды непрерывно дифференцируема на интервале [а, Ь] так, что первая производная f'(x) на этом интервале положительна и max 1«Л] < 1 f'M2 Тогда, если хп е [а, Ь] - приближение к числу г, где/(г) = 0, то значение xn+i := хп -ftxn)lf'(x^ будет к г ближе, чем хп. Последова- тельность приближений хп сходится к г (см. [Endl], п. 7.3). Если положить /(%) := х2 - с, где с > 0, то для х > 0 функция Дх) будет удовлетворять условиям сходимости метода Ньютона, а по- следовательность х -х .. 1 .л+1‘ " /U) 2 с х„ + — Хг. будет сходиться к Vc. Таким образом, получаем эффективную процедуру для приближения квадратных корней рациональными числами. Нас интересует только целая часть г числа 4с , где г2 < с < (г + I)2, а число с натуральное, поэтому при вычислении элемента последо- вательности приближений ограничимся только целой его частью.
10. Основные теоретико-числовые функции 207 .we»'-- В качестве начального приближения выберем > 4с и будем про- должать до тех пор, пока для некоторого п не получим т,1+1 > хп, то- гда хп и будет искомым значением. Естественно выбрать начальное приближение как можно ближе к 4с . Для значения с типа CLINT и ^3 , е := |_log2cJ получаем, что значение [2(с+2)/2 J всегда больше, чем 4с , то есть начальное приближение можно легко вычислить с по- мощью функции Id_1(). А вот и алгоритм. Алгоритм вычисления целой части г квадратного корня из ’ * ’ натурального числа с > 0 1. Положить х <— [_2(f+2)/2 J, где е := llog2cJ. 2. Положить у е-|_(х + с/х)/2_|. 3. Если у < х, то положить х <— у и вернуться на шаг 2. Иначе ре- зультат: х и алгоритм завершен. Доказательство корректности алгоритма не представляет труда. Зна- чение х изменяется монотонно и всегда является натуральным чис- лом. Следовательно, алгоритм в конце концов остановится. По завер- О •-ТЛ1Г шении гс ПpeдпoJ х2 > с иг Однако, у-х = что пре предпог Следую алгоритма пожим, чтс 1и, иначе, с (х + с/х) 2 ггиворечит южение не щая ф; будет ) х > г :-х2 < - х = уело] верно /нкция выполи + 1. Из; :0. с-х2 2х зию зав и х = г. [ КО| яться условие у = |_(х + с/х)/2_]> х . условия х > г + 1 > д/с следует, что <0, ершения алгоритма. Значит, наше эректно вычисляет значение r h: „ у <-[_(х + с/х)/2_], используя целочисленное деление с остатком. Функция: Целая часть квадратного корня из CLINT-объекта Синтаксис: void irootj (CLINT nJ, CLINT floorj); Вход: Выход: nJ (операнд > 0) floorj (целое число - квадратный корень из nJ) void irootj (CLINT nJ, CLINT floorj)
208 Криптография на Си и C++ в действии CLINT xj.yj, rj; unsigned I; Используя функцию Id JO и оператор сдвига, полагаем I равным L( Llog2(n_l)J + 2)/2J. Используя функцию setbitJO, полагаем у | равным 2*. I = (IdJ (nJ) + 1) » 1; SETZERO_L (yj); setbitj (yj, I); do { cpyj (xj, yj); Шаги 2 и 3. Аппроксимация методом Ньютона и проверка усло- вия завершения алгоритма. divj (nJ, xj, yj, rj); addj (yj, xj, yj); shrj (yj); } while (LT_L (yj, xj)); cpyj (floorJ, xj); Чтобы выяснить, является ли число п квадратом какого-либо числа, достаточно возвести в квадрат с помощью функции sqrj() значение функции irootJO и сравнить полученный результат с п. Если числа не совпадают, то, очевидно, п не является квадратом. Признаем, все же, что это не самый лучший метод. Существуют критерии, позво- ляющие во многих случаях распознать, является ли число квадра- том, без вычисления квадратного корня. Один из таких алгоритмов приведен в работе [Cohe]. Строятся четыре таблицы: q\l, q63, qiA и q65, в каждой из которых квадратичные вычеты по модулю П, 63, 64 и 65 помечены единицей «1», а квадратичные невычеты - нулем «О». <- 0 для к = 0, ..., 10, <?63[£] 0 для к = 0, ..., 62, ql 1[Л2 mod 11] <- 1 для£ = 0.......5, q63[k2 mod 63] <— 1 для к = 0, ..., 31,
209 рдАВА 10. Основные теоретико-числовые функции <?64[А:] <— 0 для к = 0, 63, д64[£2 mod 64] <— 1 для к = 0, 31, д65[к] <— 0 для к = 0, ..., 64, g65[&2 mod 65] <— 1 для к = 0, ..., 32. Нетрудно заметить, что, рассматривая кольцо классов вычетов как систему абсолютно наименьших вычетов (см. стр. 84), получаем ' таким образом все квадраты. Алгоритм, определяющий, является ли целое число п > О полным квадратом. Если да, то результат алгоритма - квадратный корень из числа п (|Cohe], Алгоритм 1.7.3) 1. Положить t <— п mod 64. Если q64[f] = 0, то число п не является полным квадратом и алгоритм заканчивает работу. Иначе поло- жить r<—n mod (11 • 63 • 65). 2. Если q63[r mod 63] = 0, то число п не является полным квадра- том и алгоритм заканчивает работу. 3. Если q65[r mod 65] = 0, то число п не является полным квадра- том и алгоритм заканчивает работу. 4. Если qll[rmod И] = 0, то число п не является полным квадра- том и алгоритм заканчивает работу. 5. Вычислить q <г- |_VnJ с помощью функции iroot_l(). Если q2 Ф п, то число п не является полным квадратом и алгоритм заканчи- вает работу. Иначе п является полным квадратом и результат: q - квадратный корень из п. На первый взгляд этот алгоритм может показаться странным: что за константы 11, 63, 64, 65? Внесем ясность: если целое число п явля- ется полным квадратом целого числа, то оно будет полным квадра- том и по модулю произвольного целого числа к. Воспользуемся об- ратным утверждением: если п не является квадратом по модулю к, то оно не является квадратом и в целых числах. Таким образом, на шагах 1-4 мы проверяем, является ли п квадратом по модулям 64, 63, 65 и И. Всего существует 12 квадратов по модулю 64, 16 квад- ратов по модулю 63, 21 квадрат по модулю 65 и 6 квадратов по модулю 11, то есть вероятность пропустить за четыре шага число, не являющееся полным квадратом, равна ^^Yl-—Yi-—Y1-—1 = — 16 21 6 6 641 631 65^ 11J~64 63 65 11 715' Только в таких относительно редких случаях выполняется проверка на шаге 5. Если проверка проходит успешно, то п является полным квадратом и квадратный корень из п определен. Очередность моду- лей на шагах 1-4 определяется соответствующими вероятностями. Появление следующей функции мы предвидели в п. 6.5, когда ис- ключали числа, являющиеся полными квадратами, из множества потенциальных первообразных корней по модулю р.
210 Криптография на Си и C++ в действии функция: Является ли число nJ типа CLINT полным квадратом Синтаксис: unsigned int issqrj (CLINT nJ, CLINT rj); Вход: nJ (операнд) Выход: rj (квадратный корень из nJ или 0, если nJ не является полным квад- ратом) Возврат: 1, если nJ - полный квадрат, 0, в противном случае static const UCHAR q 11 [11 ]= {1, 1,0, 1, 1, 1,0, 0, 0, 1,0}; 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,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, 0, 0, 0, 0, 0, 1,0, 0, 0, 0, 0, 0, 0, 1,0, 0, 1,0, 0, 0, 0, 1,0, 0, 0, 0, 0, 0, 0, 1,0, 0, 0, 0, 0, 0, 0, 1,0, 0, 0, 0, 0, 0}; static const UCHAR q65[65]= {1, 1,0, 0, 1,0, 0, 0, 0, 1, 1,0, 0, 0, 1,0, 1,0, 0, 0, 0, 0, 0, 0, 0, 1, 1,0, 0, 1, 1,0, 0, 0, 0, 1, 1,0, 0, 1, 1,0, 0, 0, 0, 0, 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)) { SETZERO J- (rj); return 1;
п рдДВА 10. Основные теоретико-числовые функции 211 г' ' : U. 1. ; ; if (1 == q64[*LSDPTR_L (nJ) & 63]) /* q64[nj 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 7 { irootj (nJ, r_l); sqrj (rj, qj); if (equj (nJ, qj)) { return 1; } } SETZERO_L (г_1); ; return 0; } 10.4. Квадратные корни в кольце классов вычетов Итак, мы научились вычислять квадратные корни (по крайней мере, их целые части) из целых чисел. Теперь вновь обратимся к кольцам классов вычетов, в которых займемся тем же самым, а именно, вычислением квадратных корней. При некоторых ограничениях в кольце классов вычетов существуют квадратные корни, вообще говоря, определенные неоднозначно (то есть может существовать несколько квадратных корней из одного элемента). Говоря на языке алгебры, задача состоит в том, чтобы выяснить, существуют ли для элемента а 6 Жт корни b е такие, что Р = а. Или, в тео- ретико-числовых обозначениях (см. главу 5), есть ли решения у сравнения второй степени х2 = a mod т, и если да, то какие. Если НОД(я, ni) = 1 и существует решение b такое, что b2 = a mod ли, то число а называется квадратичным вычетом по модулю т. Если
212 Криптография на Си и C++ в действии сравнение неразрешимо, то а называется квадратичным невыче. том по модулю т. Если b - решение сравнения, то b + т тоже решение, то есть можно ограничиться рассмотрением вычетов отличающихся на т. Поясним ситуацию на примере. Число 2 является квадратичным вычетом по модулю 7, поскольку З2 = 9 = 2 (mod 7); число 3 является квадратичным невычетом по модулю 5. Если число т простое, то найти квадратные корни по модулю т до- вольно просто, позже мы приведем все необходимые для этого функции. Трудность вычисления квадратных корней по модулю со- ставного числа п определяется тем, известно ли разложение числа т на простые множители. Если разложение неизвестно, то вычис- ление квадратных корней для большого числа т является матема- тически сложной задачей из класса NP (см. стр. 119), лежащей в основе безопасности ряда современных криптосистем.5 Мы еще вернемся к этому вопросу в п. 10.4.4. Определение того, является ли число квадратичным вычетом, и вы- числение квадратных корней - это две разные задачи, для решения каждой из которых существуют свои методы. В следующих пара- графах мы приведем реализацию этих методов с подробными ком- ментариями. Сначала рассмотрим процедуры, позволяющие опре- делить, является ли число квадратичным вычетом по модулю дан- ного числа. Затем научимся вычислять квадратные корни по моду- лю простых чисел и, наконец, по составным модулям. 10.4.1. Символ Якоби Сразу начнем с определения. Пусть число р Ф 2 простое и число а целое. Символ Лежандра (у) (читается «а по р») равен 1, если а - квадратичный вычет по модулю р, и -1, если а - квадратичный не- вычет по модулю р. Если а делится на р, то (у):= 0. Это определе- ние пока вряд ли нам поможет, поскольку для того, чтобы знать значение символа Лежандра, нужно знать, является ли а квадратич- ным вычетом по модулю р. Однако символ Лежандра обладает за- мечательными свойствами, которые и позволят нам оперировать с ним и, что особенно важно, вычислять его значение. Чтобы не 5 Аналогию между математической и криптографической сложностью следует проводить очень осторожно: согласно работе [Rein], вопрос о справедливости неравенства Р Ф NP весьма мало ка- сается практической криптографии. Полиномиальный алгоритм разложения на множители с вре- менной сложностью (9(и20) бессилен даже перед относительно небольшими значениями числа п, ( И0,1 тогда как экспоненциальный алгоритм сложности (9 k I справится даже с большими значе- ниями модуля. Безопасность криптографических алгоритмов на практике не зависит от того, совпадают или нет множества Р и NP, несмотря на то, что часто встречается именно такая фор- мулировка.
ГЛАВА М. Основные теоретико-числовые функции 213 сбиться с пути, не будем вдаваться в теоретические дебри. Заинте- ресованный читатель может обратиться, например, к работе [Bund], п. 3.2. Но все же нам придется привести здесь некоторые свойства, дающие основное представление о правилах вычислений с симво- лом Лежандра. (а) Число решений сравнения х2 = a (mod р) равно 1 + (7). (б) Число квадратичных вычетов и невычетов по модулю р одинаково и равно (р - 1)/2. (в) Если а = b (mod р), то (7). (г) Символ Лежандра обладает свойством мультипликативности: (Д) §(t)=o. /=1 (е) а(р"1)/2 = (-^-)(modp) (критерий Эйлера). (ж) Если число q нечетное простое и q^p, то (~)= (-1)(/’~1)(<7"1)/4^) (квадратичный закон взаимности Гаусса). (з) (f)=(-l)<p’l)/2, (|)=(-1)(',2-1)/8, У=1. Доказательство этих свойств можно найти в стандартной литерату- ре по теории чисел, например, в [Bund] или [Rose]. , 1 nr ' Сразу же приходят в голову два способа вычисления символа Лежандра. Во-первых, воспользоваться критерием Эйлера (свойст- во (е)) и вычислить ар~^12 (mod р). Для этого потребуется выпол- нять модульное возведение в степень (со сложностью <9(log3p)). , u Во-вторых (и это более разумное решение), с помощью квадратич- ного закона взаимности реализовать следующую рекурсивную про- цедуру, основанную на свойствах (в), (г), (ж) и (з). Рекурсивный алгоритм вычисления символа Лежандра для целого а и нечетного простого р 1. Если а = 1, то (7) = 1 (свойство (з)). 2. Если а четное, то (^)= (-1)(р'-1)/8(^) (свойства (г), (з)). 3. Если аФ 1 и а = qx...qk - произведение нечетных простых <7ь---,дьто
214 Криптография на Си и C++ в действии Для каждого i вычисляем j _ 1У Р-1)(<7/ -1)/4 (р inod Qi с помощью шагов 1-3 (свойства (в), (г) и (ж)). Прежде чем заняться программной реализацией этого алгоритма, рассмотрим обобщение символа Лежандра. Это позволит нам обой- тись без разложения на простые множители, которое необходимо при использовании квадратичного закона взаимности в виде свой- ства (ж) и для больших чисел занимающее чересчур много времени (о задаче разложения см. стр. 225). А для этого обратимся к нере- курсивной процедуре и введем еще одно определение. Для целых чисел а и b =р{...рк9 где числа простые, но не обязательно раз- личные, символ Якоби (называемый также символом Якоби - Кро- некера, или символом Кронекера - Якоби, или символом Кронекера) (f) определяется как произведение символов Лежандра : fe):=nk). /=1 где О, если а четное, М-= \2/’ ,2 п/о (-iya -V , если я нечетное. Для полноты картины определим (f):=l для а е Z; (f):=l, если а = ±1, и (§•):= 0 в противном случае. Если число b нечетное простое (то есть k= 1), то значения симво- лов Лежандра и Якоби совпадают. В этом случае символ Якоби (Лежандра) показывает, является ли а квадратичным вычетом по модулю Ь, то есть существует ли число с такое, что с2 = a mod b. Если такое с существует, то (f) = 1, иначе (f)=-l (или (f) = 0, ес- ли а = 0 mod b). Для составного числа b (при к > 1) число а является квадратичным вычетом по модулю b тогда и только тогда, когда НОД(я, b) = 1 па является квадратичным вычетом по модулю всех простых чисел, делящих Ь, то есть когда для всех / = 1,...» к. Ясно, что это не эквивалентно равенству (f)=l. Например, срав- нение х2 = 2 mod 3 неразрешимо, то есть (|)= -1. Но по определе- нию (|)= 1 > хотя сравнение х2 = 2 mod 9 также неразрешимо. Обратно, если (g-)= -1, то а в любом случае является квадратичным
рддВА 10. Основные теоретико-числовые функции 215 невычетом по модулю Ь. Равенство (f) = 0 равносильно тому, что НОД(л,/>)*1. Пользуясь свойствами символа Лежандра, выведем свойства сим- вола Якоби: (a) и, если/, •<?*(), то (^)=(fXf)- (б) Если а = с mod b, то (%) = (f) • (в) Для нечетных b > 0 справедливы равенства Й=(-1)М1. (i)=i (см. свойство (з) символа Лежандра). (г) Для нечетных а и Ь, где b > 0, выполняется квадратичный закон вза- имности (см. свойство (ж) символа Лежандра): (f) = (-l)(a“1)(Z,-1)/4 Из этих свойств (доказательство см. в литературе, указанной выше) получаем следующий алгоритм Кронекера (см. [Cohe], п. 1.4) вы- числения символа Якоби (или, в зависимости от условий, символа Лежандра) для двух целых чисел. Этот алгоритм нерекурсивный. Кроме того, чтобы не зависеть от знаков этих чисел, положим (тр):= 1 при а > 0 и (тр):= -1 при а < 0. Алгоритм вычисления символа Якоби (f) для целых чисел а и Ъ 1. При b = 0 завершить алгоритм с результатом 1, если |а| = 1, и с результатом 0 в противном случае. 2. Если оба числа а и b четные, то завершить алгоритм с результа- том 0. Иначе положить v <— 0 и, пока b четное, выполнять: v <— v + 1 и b <— Ы2. Если теперь v четное, то положить к <— 1; иначе положить к <— (-1)(а -1)^8. При b < 0 положить b <—Ь. При а < 0 положить к <------к (см. свойство (в) символа Якоби). 3. При <7 = 0 завершить алгоритм с результатом 0, если b > 1, с результатом к в противном случае. При а Ф 0 положить v <— 0 и, пока а четное, выполнять: v <— v + 1 и а <— а/2. Если теперь v нечетное, то положить к <— (-1)(/? -1)/8 • к (см. свойство (в) сим- вола Якоби). 4. Положить к <— (-1)(а-1)(/?~1)/4 • к , r<- |а|, а <— b mod г, b <— г и вернуться на шаг 3 (см. свойства (б) и (г) символа Якоби). Время работы этого алгоритма равно 6>(log2TV), где число N>a,b- верхняя граница чисел а и Ь. Это значительно лучше, чем вычисле-
216 Криптография на Си и C++ в действии ние по критерию Эйлера. А вот еще два штриха, улучшающие этот алгоритм (см. [Cohe] п. 1.4): ✓ Для вычисления значений (~1)(а и -1)/8 на шагах 2 и 3 лучше заготовить таблицу предвычислений. ✓ Значение (-1)(а~1)(/,-1)/4 . £ На шаге 4 можно вычислить на языке С как if (a&b&2) к = -к, где & - это поразрядная операция AND. В обоих случаях не нужно явно возводить в степень, что, конечно, благоприятно сказывается на времени работы алгоритма. Остановимся на первом «штрихе» более подробно. Если на 2 мы полагаем к равным (-1)(“ -1)/8, то а нечетное. То же справедливо в отношении b на шаге 3. Для нечетного а 2 | (а - 1) и 4 | (я + 1) или 4 | (а - 1) и 2 | (а + 1), то есть 8 делит произведение (а - 1)(а + 1) = а2 - 1 и число (а2 - 1 )/8 - целое. Кроме того, выполняется равенство (-1)(а“-1)/8 = (для проверки достаточно подставить в показатель степени выра- жение а = к • 8 + г). Следовательно, показатель определяется лишь четырьмя значениями числа a mod 8 = ±1 и ±3, дающими соответ- ственно результаты 1, -1, -1 и 1. Записываем их в виде вектора {О, 1, 0, -1, 0, -1, 0, 1}, откуда, зная a mod 8, можем найти значение (-!)((“ mtxisr-n/s заметим? что а mod 8 можно представить в виде выражения а & 7, где & по-прежнему означает бинарную операцию AND, то есть возведение в степень сводится к нескольким быстрым процессорным операциям. Для пояснения второго «штриха» заметим, что (а & b & 2) * 0 то- гда и только тогда, когда числа (а - 1 )/2 и (b - 1 )/2, а значит и (а - 1)(Z? - 1 )/4, нечетные. И, наконец, введем вспомогательную функцию twofactJO для вы- числения значений v и b на шаге 2 для четного Ь, а также v и а на шаге 3 для четного а. Функция twofactJO находит представление числа типа CLINT в виде произведения степени двойки и нечетного числа.
ГЛАВА 10. Основные теоретико-числовые функции 217 функция: Представление CLINT-объекта в виде а = 2ки, где число и нечетное Синтаксис: int twofactj (CLINT aj, CLINT bj); Вход: aj (операнд) Выход: bj (нечетная часть числа aj) Возврат: к (логарифм по основанию 2 в разложении числа а_1) . int twofactj (CLINT a_l, CLINT bj) { int k = 0; if (EQZ_L (aj)) SETZERO_L (bj); return 0; } cpyj (bj, aj); while (ISEVENJ- (bj)) { shrj (bj); ++k; } fc*’ 1 return k; } 1 -аиаимймЧ' 4 • < Теперь мы вооружены всем необходимым для реализации функции jacobiJO, вычисляющей символ Якоби. Функция: Вычисление символа Якоби от двух CLINT-объектов Синтаксис: int jacobij (CLINT aaj, CLINT bbj); Вход: aaj, bbj (операнды) Возврат: ±1 (значение символа Якоби aaj по bbj)6 Нетрудно заметить, что она возвращает еще и 0. - Прим. ред.
218 Криптография на Си и C++ в действии *—. 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; } } t J Шаг 2. Удаляем четную часть переменной bbj. if (ISEVEN_L (aaj) && ISEVENJ. (bbj)) { return 0; } cpyj (aj, aaj); cpyj (bj, bbj); v = twofactj (bj, bj); if ((v & 1) == 0) /* v четное? */ {
р\ДВА 10. Основные теоретико-числовые функции к= 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) 1=0) к = tab2[*LSDPTR_L (bj) & 7]; Ul .• '-’О Шаг 4. Применяем квадратичный закон взаимности. R if (*LSDPTR_L (aj) & *LSDPTR_L (bj) & 2) k = -k; cpyj (tmpj, aj); mod J (bj, tmpj, aj); cpyj (bj, tmpj); Km ! * } GO’ e jf (GT_L one_|)J { k = 0; } return (int) k;
220 Криптография на Си и C++ в действие 10.4.2. Квадратные корни по модулю рк Теперь мы знаем, что целое число может быть квадратичным выче. том или невычетом по модулю другого целого числа. Более того, у нас есть эффективная программа, определяющая, какой из этих случаев имеет место. Но даже зная, что целое число а является квадратичным вычетом по модулю целого /г, мы пока не умеем из- влекать квадратный корень из а, особенно если число п большое. Будем скромны и попытаемся сначала сделать это для простых п. Таким образом, наша задача - решить сравнение второй степени (10.11) х2 = a mod р, где число р нечетное простое и а - квадратичный вычет по модулю р (это гарантирует нам, что сравнение разрешимо). Рассмотрим два случая: р = 3 mod 4 и р = 1 mod 4. В первом, более простом случае решением сравнения будет х := a(p+l)/4 mod р, поскольку (10.12) х2 = а(р+>>12 = а ар^'>/2 = a mod р, где а(р~^2 = (-^)=lmodp мы вычислили по свойству (е) символа Лежандра (критерий Эйлера). Следующие рассуждения, заимствованные из [Heid], приводят нас к общей процедуре решения сравнений второй степени, в том числе и в случае р = 1 mod 4. Представим р - 1 в виде р - 1 = 2kq, где к > 1 и число q нечетное. Найдем произвольный квадратичный невычет п mod р, выбирая случайное л, 1 < п < р, и вычисляя символ Лежандра Значение -1 мы получим с вероятностью у, то есть нужное п будет найдено довольно быстро. Положим х0 = я(</+1)/2 mod р, (10.13) у о = п1 mod р, Zq = a1 mod р, г0 •= к. Согласно малой теореме Ферма a(p~i)/2 = x2(p~l)/2 = xp~i = 1 mod р, если х - решение сравнения (10.11). Кроме того, если п - квадратичный невычет по модулю р, то н^~1)/2 = -1 mod р (см. свойство (е) символа Лежандра, стр. 213). Тогда
10. Основные теоретико-числовые функции 221 azQ - хо mod /Л ' 'ГЛ Уо ° = -1 mod р, 2Г°-1 z0 = 1 mod р. Если zq = 1 mod р, то х0 будет решением сравнения (10.11). Иначе определим рекуррентные последовательности х,, yh zi, гь где (10.15) azi = xj mod р, 2^/ ”1 у, = -1 mod p, 2r'-1 Zj = 1 mod p и г, > Сделав не более чем к шагов, получим Zi = 1 mod р, то- гда Xi будет решением сравнения (10.11). Для этого найдем т0 - 2т° 1 д наименьшее натуральное число, для которого zQ =1 mod р, то есть ш0 < го - 1. Положим (10.16) х/+1 = xiyi mod р, yi+i = У, mod Р> z,+i = mod р, где г,+1 := /п, := min|/i > 11 z2” = 1 mod р}. Тогда (10.17) 2 2 2r'~w' 2r,-w' xi+i = xi yi = “Wi = ^/+1 mod z x2"''-1 2r,+1-i 2"’r-i ( 2ri-"4 ) 2/'“1 1 J Л+i =Л-+1 =1л- 1 =y> =-lmodp, Qtnj -1 7r/+l-l ( jn-пч \ 9W/-I zz+i =zi+i =^z,>,- j =-Zi ^Imodp, поскольку (z2 ' ) = zf ’ = 1 mod p , и значит, в силу минимально- сти mh возможен лишь случай zj ' = -1 mod р . Таким образом, доказана корректность алгоритма Д. Шенкса (D. Shanks), определяющего решение сравнения второй степени (см. [Cohe], Алгоритм 1.5.1).
222 Криптография на Си и C++ в действии Алгоритм извлечения квадратного корня из целого числа а по модулю нечетного простого р 1. Записать р - 1 в виде 2kq, где число q нечетное. Найти случайное п такое, что (^-)= -1. 2. Положить х <— a(^I)/2 mod р, у <— nl mod р, z<r- а • х2 modр, b' J (’ •.; ‘ > X 4 х <— а • х mod р и г <— к. 3. Если 2=1 mod р, то завершить алгоритм с результатом х. Иначе найти наименьшее т, для которого 22 =1 mod р . При т = г завершить алгоритм с результатом «а - квадратичный невычет по модулю р». 4. Положить t <— у2' т 1 mod р , у <— t2 mod р, г <— т mod р, х <г- х • t mod р, z <— z • у mod р и вернуться на шаг 3. Ясно, что если х - решение сравнения второй степени, то -х mod р тоже будет решением этого же сравнения, так как (-л)2 = х2 mod р. В следующей программной реализации, не задумываясь о практич- ности, для всех подряд натуральных чисел, начиная с 2, будем вы- числять символ Лежандра, надеясь найти квадратичный невычет по модулю р за полиномиальное время. Наши надежды оправдаются, если считать, что верна до сих пор не доказанная расширенная ги- потеза Римана (см., например, [Bund], п. 7.3, Теорема 12 или [КоЫ], п. 5.1 или [Кгап], п. 2.10). Насколько мы сомневаемся в справедли- вости расширенной гипотезы Римана, настолько является вероятно- стным алгоритм Шенкса. При построении функции prootJO не будем принимать во внимание эти соображения и просто будем считать, что время работы алго- ритма полиномиально. Дальнейшие подробности см. в работе [Cohe], стр. 33 и далее. Функция: Извлечение квадратного корня из а по модулю р Синтаксис: int prootj (CLINT aj, CLINT pj, CLINT xj); Вход: aj, pJ (операнды, число pJ > 2 - простое) Выход: x_l (квадратный корень из aj по модулю pj) Возврат: 5 0, если aj - квадратичный вычет по модулю pj, -1 в противном случае int prootj (CLINT aj, CLINT p_l, CLINT xj) { CLINT b_l, q_l, t_l, y_l, z_l;
10. Основные теоретико-числовые функции 223 int г, т; if (EQZ_L (pj) || ISEVENJ. (pj)) return -1 ; Если aj == 0, то результат: 0. if (EQZ_L (aj)) SETZERO J_ (xj); return 0; Шаг 1. Находим квадратичный невычет. cpyj (qj, pj); dec J (qj); r = twofactj (qj, qj); cpyj (zj, two J); while (jacobij (zj, pj) == 1) *W> incj (zj); mexpj (zj, qj, zj, pj); Шаг 2. Инициализация рекуррентной последовательности. cpyj (yJ, zj); dec J (qJ)>*
224 Криптография на Си и C++ в действии shrj (qj); mexpj (aj, qj, xj, pj); msqrj (xj, bj, pj); mmulj (bj, aj, bj, pj); mmulj (xj, a J, xj, pj); c Шаг 3. Завершение процедуры; в противном случае находим наи- меньшее т, для которого z2 = 1 mod р . modj (bj, pj, qj); while (lequj (qj, onej)) { m = 0; do { ++m; msqrj (qj, qj, pj); .f ' 1 \ * A *«; / while (lequj (qj, onej)); if (m == r) { return -1; } Шаг 4. Рекуррентная формула для х, у, z и г. mexp2J (yj, (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; 11
f-дДВА 10. Основные теоретико-числовые функции 225 } return 0; } С учетом результатов, полученных для модуля р, мы теперь можем извлекать квадратные корни по модулю рк. Для этого рассмотрим сначала сравнение (10.18) х2 = a mod р2. Если %! - решение сравнения х2 = а mod р, то, полагая х :=xi+ р • х2, получаем < 2 Л 2 2 22 л 12 х - а = х{ - а + 2рхрс2 + р х2 = р — h 2хрс2 mod р , 1 р J то есть для решения сравнения (10.18) нам нужно найти решение х2 п • • ог . линейного сравнения _ х,2 - а ~ , х • 2xj + —! = 0 mod р . Р Продолжая рекурсивно, за конечное число шагов получаем решение сравнения х = a mod рк для любого к е IN. р • .,, ОС. 10.4.3. Квадратные корни по модулю п Мы сделали важный шаг - научились извлекать квадратные корни по модулю простого числа - на пути к конечной цели - решению сравнения х = a mod п для составного числа п. Справедливости ради отметим, что эта задача не из легких. В принципе, она разрешима, но требует значительных вычислений, объем которых увеличивается экспоненциально с ростом п. Решить это сравнение трудно на- столько (в теоретико-сложностном смысле), насколько трудно раз- ложить число п на простые множители. Обе задачи лежат в классе NP (см. стр. 119). Следовательно, извлечение корней по модулю составных чисел связано с задачей, которая на сегодняшний день не разрешима за полиномиальное время. И вряд ли мы сможем ре- шить сравнение быстро для больших п. Тем не менее, если есть два сравнения второй степени: у2 = a mod г и z2 = a mod s, где числа г и 5 взаимно просты, то можно объединить ре- шения этих сравнений и получить решение сравнения х = a mod rs. В этом нам поможет китайская теорема об остатках’. Пусть натуральные числа mj, ..., тг попарно взаимно просты (то есть НОД(ть пц) = 7 при пц ^nij) и числа a]f ..., аг - произвольные целые. Тогда существует решение системы сравнений х =ai mod пц, причем это решение единственно по модулю произведения трп^-.т^
226 Криптография на Си и C++ в действия — Хотелось бы уделить немного времени доказательству этой теоре. мы, поскольку именно в доказательстве сокрыто обещанное реше- ние. Положим т := niiin2...mr и in j := mlnij. Тогда число nij целое и НОД(/н < nij) = 1. Из п. 10.2 мы знаем, что существуют целые числа Uj и Vj такие, что 1 = m'jUj + nijVj, то есть m'jUj = 1 mod nij для j = 1, ... qVii г, и умеем их вычислять. Построим сумму x0:=X/zW/’ W J=1 тогда, в силу сравнения m'jUj = 0 mod ди, для i Ф j, получаем оконча- тельное решение: (10.19) г хо := ^m'jujaj = = at mod mi . j=i Предположим, что существуют два решения: х0 = a, mod и Xi = a,} mod имеем х0 = -И m°d nih или, эквивалентно, разность %о - делится одновременно на все mh то есть на наименьшее об- щее кратное чисел п^. А так как числа попарно взаимно просты, их наименьшее общее кратное есть не что иное как произведение всех этих чисел, и х0 = хл mod т. С помощью китайской теоремы об остатках будем искать решение сравнения х = a mod rs, при этом НОД(г, д) = 1, числа гид- нечет- ные простые и ни г, ни д не делят а. Пусть уже известны решения ’: 1 C шл сравнений у2 = a mod г и z = a mod д. Найдем общее решение срав- нений х = у mod г, х = z mod 5 . < - '< ij как xq := zur + yvs mod rs, где 1 = иг + vs - линейное представление наибольшего общего , > 0! делителя чисел гид. Тогда х} = a mod г и х^ = a mod д, а так как НОД(г, s) = 1, то будет верно и сравнение х}=а mod rs и мы нашли решение сравнения второй степени. Как мы уже говорили, каждое из сравнений по модулю гид имеет два решения, именно ±У и ±z, тогда, подставляя эти значения в выражение для х0, получаем четыре решения сравнения по модулю rs: (10.20) xq := zur + yvs mod гд, (10.21) Х\ := -zur - yvs mod rs = -Xq mod гд,
гдДВА 10. Основные теоретико-числовые функции 227 ***** (10.22) х2 := -zur + yvs mod rs, ”(10.23) x3 := zur - yvs mod rs = -x2 mod rs. s И i Таким образом, мы можем свести решение сравнений второй сте- пени вида х2 = a mod и, где число п нечетное, к случаю х2 = a mod р с простым р. Для этого найдем разложение п = pkl ...рк< и вычислим корни по модулям ph из которых с помощью рекурсивной процедуры п. 10.4.2 находим решения сравнений х2 = a mod р-1 . И последний аккорд: по китай- ской теореме об остатках объединяем эти решения в решение срав- нения х2 = a mod п. Функция, которую мы сейчас рассмотрим, находит решение сравнения х2 = a mod п именно таким способом. Единственное ограничение: мы предполагаем, что п= р • <у, где числа р и q - нечетные простые. Сначала ищем решения и х2 сравнений х2 = a mod р, х2 = а mod q, а затем восстанавливаем из хл и х2 решения сравнения •-АГЛЛу х = a mod pq рассмотренным выше методом. В качестве квадратного корня из а по модулю pq берем наименьшее из полученных значений. Функция: Извлечение квадратного корня из а по модулю р • q, где числа р и q - нечетные простые Синтаксис: int rootj (CLINT aj, CLINT pj, CLINT qj, CLINT xj); Вход: aj, p_l, qj (операнды, простые числа pj, qj > 2) Выход: xj (квадратный корень из aj по модулю pj * qj) Возврат: 0, если a J - квадратичный вычет по модулю pj * qj, -1 в противном случае int rootj (CLINT aj, CLINT pj, CLINT qj, CLINT xj) { CLINT xOJ, x1 J, x2J, x3J, xp_l, xq_l, n_l; CLINTD uj, vj;
228 Криптография на Си и C++ в действии clint *xptrj; int sign_u, sign_v; I Вычисляем корни по модулям pl и qj с помощью функции prootJO. При а_1 == 0 результат: 0. if (0 != prootj (а_1, р_1, хр_1) || 0 != prootj (aj, qj, xq_l)) { return -1; } if (EQZ_L (aj)) { SETZERO J- (xj); return 0; } Для корректного применения китайской теоремы об остатках, следует учесть знаки чисел uj и vl. Для задания этих знаков вве- дем вспомогательные переменные sign_u и signv, значения ко- торых будем вычислять с помощью функции xgcdJO. Результа- том этого шага является корень х0. ? mulj (pj, qj, nJ); I xgcdj (pj, qj, xOJ, uj, &sign_u, vj, &sign_v); 5 mulj (uj, pj, uj); i j 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); Теперь находим корни x1z x2 и x3. subj (nJ, xOJ, x1 J); msubj (uj, vj, x2J, nJ);
229 |-дДВА 10. Основные теоретико-числовые функции subj (nJ, x2J, x3J); Наименьшее значение берем в качестве результата. xptrj = MIN_L (xOJ, х1 J); xptrj = MIN-L. (xptrj, x2J); xptrj = MIN_L (xptrj, x3J); cpyj (xj, xptrj); return 0; ’ЗОЯ Теперь мы легко можем реализовать китайскую теорему об остат- ках, обобщив только что рассмотренную функцию на случай боль- шего числа переменных. Это и сделано в следующем алгоритме, принадлежащем Гарнеру (Garner) (см. [MOV], стр. 612). Преиму- щество этого алгоритма перед приведенным выше состоит в том, что остатки вычисляются только по модулям а не по модулю т = niim2...mr, что значительно сокращает время вычислений. и Алгоритм 1 решения системы линейных сравнений х = mod mi9 1 < i < г, где НОД(/лг, mj) = 1 при i * j 1. Положить и <— «1, x <— и и i <— 2. 2. Положить Ci <— 1, j <- 1. 3. Вычислить и <— m/1 mod nii (расширенным алгоритмом Евклида; см. стр. 202) и Ci <— uCi mod пц. 4. Положить j <—j + 1. При j < i - 1 вернуться на шаг 3. /-1 5. Положить и <— (а, - х)С, mod п^ и х <— х + • J=i 6. Положить i <— i + 1. При i < г вернуться на шаг 2. Иначе результат: х Вообще говоря, не очевидно, что этот алгоритм делает именно то, что нужно. Докажем его корректность по индукции. Пусть г = 2, тогда на шаге 5 х = «j + ((а2 - аС)и mod ni2)ni[. Сразу видно, что х = сц mod пц. Чуть менее тривиально х = «1 + (а2 - а^т^тС1 mod т2) = а2 mod т2.
230 Криптография на Си и C++ в действии — Для выполнения индукционного перехода от г к г + 1 предположи^ что алгоритм выдает нужный результат хг для некоторого г >£ и добавим еще одно сравнение x = «r+i mod тг+\. Тогда на шаге 5 получаем /7 г > А г х = хг+ (аг+1-х)П'»;' modmr+1 П"Ь К И - J J *• | По индукционному предположению, х = xr = a, mod m, для 1 = 1, ,,L r Кроме того, * 11 11 Х = хг+ (аг+1 - х)П"Ь • П"Ь • = аг+1 mod '«г+1 > к 7=1 •/=1 > что и требовалось доказать. Для практической реализации китайской теоремы об остатках вос- пользуемся одной очень хорошей функцией, в которой не нужно заранее задавать число сравнений - его можно ввести во время ра- боты программы. Модифицируем процедуру, приведенную выше. При этом мы, увы, теряем возможность вычислять только по моду- лям пщ но зато можем оперировать с переменным числом парамет- ров щ и nii системы сравнений при фиксированных затратах памяти. Вот этот алгоритм (см. [Cohe], п. 1.3.3). Алгоритм 2 решения системы линейных сравнений х = сц mod 1 < i < г, где НОД(т/, ал7) = 1 при i j 1. Положить i <— 1, т <— т\ и х <— а\. 2. Если i = г, то закончить алгоритм с результатом х. Иначе по- ложить i <— i + 1 и найти расширенным алгоритмом Евклида (см. стр. 199) ииу такие, что 1 = uni + упщ 3. Положить х <г— шпщ + утре, т <— пищ, х <— х mod пг и вернуться на шаг 2. Этот алгоритм становится понятен уже для случая трех сравнений: х = щ mod nii, i= 1,2, 3. Для i = 2 получаем на шаге 2 1 = U\l1l\ + У\Ш^, на шаге 3: jq = mod Ш1/И2. При следующем прохождении цикла для i = 3 обрабатываем пара- метры а3 и т3. Получаем на шаге 2 1 = U2Ul + У^Щ = U2nilni2 + V2^3
ГЛАВА 10. Основные теоретико-числовые функции 231 RN , s •/‘Г и на шаге 3 х2 - и2та2 + V2"^i rood тт\ - = + v2m2Uitnia2 + г2аизVim2ai mod /нрлг^з- Слагаемые u2wii/n2^3 и уходят, если взять вычет х2 mod т{. Кроме того, г2^з = vpn2 = 1 mod т\ по построению, сле- довательно, х2 = mod mi есть решение первого сравнения. Анало- гичными рассуждениями можно показать, что х2 является решени- ем двух других сравнений. Реализуем индуктивный способ построения решения в виде функ- ции chinrem_l(), позволяющей применять китайскую теорему об ос- татках с переменным числом сравнений. Строим вектор четной длины указателей на CLINT-объекты ah /щ, а2, т2, а2, ... - коэф- фициентов системы сравнений х = mod Поскольку ее решение имеет длину порядка ^.logm, , при большом числе сравнений или размере параметров может возникнуть переполнение. Такие ошиб- ки придется распознавать и сообщать о них при возвращении зна- чения функции. Функция: Применение китайской теоремы об остатках для решения системы линейных сравнений Синтаксис: int chinremj (int noofeq, clint **koeff_l, CLINT x_l); Вход: noofeq (число сравнений) koeffj (вектор указателей на С LI NT-коэффициенты пц сравнений х = сц mod т^ где i = 1, ..., noofeq) Выход: х_1 (решение системы сравнений) Возврат: ' >'< л" E_CLINT_OK, если все порядке E_CLINT_OFL в случае переполнения 1, если значение noofeq равно 0 2, если числа не попарно взаимно просты int chinremj (unsigned int noofeq, clint** koeffj, CLINT xj) { clint *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)
232 Криптография на Си и C++ в действии { return 1; } Инициализация. Обрабатываем коэффициенты первого сравнения. cpyj (xj, *(koeffj++)); cpyj (mJ, *(koeffj++)); Если есть еше сравнения, то есть noofeq > 1, то обрабатываем параметры остальных сравнений. Если хотя бы одно из значений mij не взаимно просто с предыдущими модулями, входящими в произведение mJ, то функция завершается с кодом ошибки 2. for (i = 1; i < noofeq; i++) { aij = *(koeffj++); mij = *(koeffj++); 4 xgcdj (mJ, mij, gj, uj, &sign_u, vj, &sign_v); if (!EQONE_L(gJ)) { ‘ ; return 2; i Будем сохранять ошибку переполнения. По завершении функции код ошибки будет храниться в переменной error. err = mulj (uj, mJ, uj); if (E_CLINT_OK == error) { error = err;
233 ГЛАВА 10. Основные теоретико-числовые функции err = mulj (uj, aij, uj); if (E_CLINT_OK == error) .1' { uj/’' J- error = err; 4 \\ / q err = mulj (vj, mij, vj); if (E_CLINT_OK == error) - { , error = err; } err = mulj (vj, xj, vj); if (E_CLINT_OK == error) error = err; / W' Побеспокоимся о знаках sign_u и sign_v (или sign_x) переменных uj и vj (или xj) и снова воспользуемся вспомогательными функциями sadd() и smod(). м sign_x = sadd (uJ, sign_u, vj, sign_v, xj); ) д л err = mulj (mJ, mij, mJ); •рК.'УЦ? Пл, if (E_CLINT_OK == error) { error = err; 3i! Л ( .. smod (xj, sign_x, m_l, x_l); } ,v‘ ;ХиЛн>. '.i 1 ' rij' . 'X return error;
234 Криптография на Си и C++ в действии 10.4.4. Квадратичные вычеты в криптографии А вот и обещанные (на стр. 212) примеры применения квадратич- ных вычетов и квадратных корней в криптографии. Сначала рас- смотрим протокол шифрования Рабина, а затем схему аутентифи- кации Фиата-Шамира.7 Безопасность протокола шифрования, опубликованного в 1979 г. Майклом Рабином (Michael Rabin) (см. [Rabi]), основана на слож- ности задачи извлечения квадратных корней в кольце Очень важно, что доказано, что эта задача эквивалентна задаче разложе- ния на множители (см. также [Кгап], п. 5.6). Этот протокол весьма прост в реализации, поскольку требует лишь возведения в квадрат по модулю п. Генерация ключей для протокола Рабина 1. Участник А генерирует два больших простых числа р ~ q и вы- числяет пь = р • q. 2. Открытым ключом для А является число нд, секретным ключом - пара (р, q). Участник В может послать участнику А сообщение М Е £п, зашиф- рованное на открытом ключе пд. Протокол Рабина шифрования с открытым ключом 1. Участник В, используя функцию msqr_l() со стр. 92, вычисляет С := М2 mod ид и посылает участнику А шифртекст С. 2. Чтобы расшифровать полученное сообщение, А вычисляет четыре квадратных корня Mi из С по модулю ид (/ = 1, ..., 4) с помощью функции rootj() (см. стр. 227), при этом функция должна выдавать не наименьшее значение квадратного корня, а все четыре значения.8 Один из этих корней и есть исходный открытый текст М. Теперь перед А встает вопрос: какой же из четырех корней Л/, соот- ветствует открытому тексту ml Если А и В предварительно догово- рятся о некоторой избыточности сообщения М, скажем, в сообще- нии М последние г бит должны быть одинаковыми, то у А не будет 7 Основные понятия, необходимые для понимания криптографии с открытым ключом, читатель по-прежнему найдет в главе 16. 8 Не умаляя общности, можно считать, что НОД(Л/, лд) = 1 и, значит, существует четыре различ- ных корня из С. В противном случае отправитель В может разложить число пд на множители, вычислив НОД(Л/, яд), что, разумеется, совершенно недопустимо.
ГЛАВА 10. Основные теоретико-числовые функции 235 л-т д е/ ОЩфМ!; ИН'го Д WtamqTOhr проблем с выбором правильного текста (вероятность того, что оди- наковыми будут г бит в одном из трех остальных текстов, пренеб- режимо мала). Кроме того, избыточность позволяет предотвратить следующую атаку на протокол Рабина. Если нарушитель X выберет случайное число Яе Z*a и сможет получить от А (не важно под каким пред- логом) один из корней сравнения X := R2 mod ид, то с вероятно- стью у будет выполняться сравнение Rj £ ±7? mod пк . Из соотношений пА = р • q | (R2 - R2) = (7?z - 7?)(7?( + R) * 0 получа- ем 1 * НОД(7? - Rb ид) е {/?, q}> то есть X может вскрыть ключ, раз- ложив на множители число пА (см. [Bres], п. 5.2). Если же открытый текст обладает избыточностью, то А всегда может понять, какой из корней соответствует истинному открытому тексту. Максимум, что может сделать А - это открыть нарушителю значение R (при усло- вии, что R имеет нужный формат), совершенно для X бесполезное. При практическом использовании этого протокола недопустим умышленный или случайный доступ к значениям квадратных кор- ней из шифртекста. Еще один пример применения квадратичных вычетов в криптогра- фии - протокол аутентификации Амоса Фиата (Amos Fiat) и Ади Шамира (Adi Shamir), опубликованный в 1986 г. и специально предназначенный для использования в смарт-картах. Пусть I - по- следовательность символов, которые содержат информацию, иден- тифицирующую пользователя А, т - произведение двух больших простых чисел р и <?, fiZ, п) -> Ж!П - случайная функция, отобра- жающая произвольные конечные последовательности символов Z и натуральных чисел п в кольцо классов вычетов некоторым не- предсказуемым образом. Простые делители р и q числа т известны только удостоверяющему центру. Рассмотрим алгоритм генерации удостоверяющим центром компонентов ключа по информации I и пока еще не определенному числу к G IN. Алгоритм генерации ключей для протокола Фиата-Шамира 1. Вычислить значения =/(/, /) е Zm для некоторого i > к 6 IN. 2. Выбрать к различных квадратичных вычетов из vz и вычислить наименьшие значения 5,-,...,^ квадратных корней из v,V,;1 в2и. 3. Сохранить значения I и sik с защитой от несанкциониро- ванного доступа (например, на смарт-карте).
236 Криптография на Си и C++ в действии Для выработки ключей можно воспользоваться функциями jacobiJO и rootj(), а в качестве функции f взять одну из хэщ~ функций из главы 16 (например, RIPEMD-160). Как сказал однажды на конференции Ади Шамир: «Сойдет любая безумная функция». Используя информацию, хранящуюся у удостоверяющего центра на смарт-карте, участник А может быть аутентифицирован участ- ником В. Протокол аутентификации а-ля Фиата-Шамира 1. Участник А отправляет участнику В значения I и ij, где j = 1, . к. 2. Участник В генерирует значения = f(I,ij) g для j = 1, к. Далее для т= 1, ..., t выполняются шаги 3-6 (значение t пока не определено). 3. Участник А выбирает случайное число rTG 7Lm и отправляет участнику В значение хх = гх . 4. Участник В отправляет участнику А двоичный вектор (^Т1 ’ • •»еч). 5. Участник А отправляет участнику В числа ух := rx =isz g Zw. 6. Участник В проверяет, что хх := ух J*J _^vi . Если А действительно знает числа sik, то на шаге 6 выполня- ется последовательность равенств в кольце Ут ГЕ = Гг21Ъ'2 ’ ПУ = = Гг (>Т(=1 ст.=1 fT.=l и, таким образом, участник А может доказать свою подлинность участнику В. Нарушитель, стремящийся узнать информацию об А, может с вероятностью 2~kt угадать вектор (е,..е ), отправляе- мый участником В на шаге 4, а для этого предусмотрительно по- слать на шаге 3 участнику В значение хх = гт2Р[ _jVf-. Например, при k = t= 1 вероятность успеха для нарушителя будет равна То есть значения к и t нужно выбирать так, чтобы вероятность успеха для нарушителя была практически нулевой и чтобы (в зави- симости от приложения) получить подходящие значения для: - длины секретного ключа; - объема данных, передаваемых между А и В; J - временной сложности, определяемой числом умножений. Я
ГЛАВА 10. Основные теоретико-числовые функции 237 Подходящие значения параметров приведены в работе [Fiat] для различных к и t при kt = 72. ;'1' Итак, безопасность рассмотренного протокола определяется защи- щенностью значений , выбором параметров к и t и сложностью задачи разложения: тот, кто сумеет разложить модуль т на мно- жители, сможет вычислить и компоненты секретного ключа. Значит, модуль т нужно выбрать так, чтобы его трудно было разло- жить на множители. И здесь мы снова отсылаем читателя к главе 16, с где будет обсуждаться проблема генерации модулей для криптоси- стемы RS А. 1 Отметим, что в протоколе Фиата - Шамира участник А может про- ходить аутентификацию сколь угодно часто, не выдавая при этом никакой информации о секретном ключе. Подобные алгоритмы называются доказательством с нулевым разглашением (см., напри- гг - мер, [Schn], п. 32.11). 10.5. Проверка на простоту Не буду больше томить Вас неизвестностью - самое большое число Мерсенна, Ml 1213, по- моему, самое большое из известных на сегодня простых чисел, содержит 3375 разрядов, то есть равно примерно Т-281 4-.9 Айзек Азимов, Дополнительное измерение, 1964 Число 26972593 - 1 - простое!!! http://www.utm.edu/research/primes/largest.html (Май 2000)'° Изучение простых чисел и их свойств - одно из старейших направ- лений в теории чисел и имеет огромное значение для криптографии. Вроде бы безобидное определение: простое число - это натуральное число, большее 1 и делящееся только на само себя и 1, - а сколько оно породило проблем и вопросов. Вот некоторые из них: «Сколько существует простых чисел?», «Как простые числа распределены в множестве натуральных чисел?», «Как можно проверить число на Через Т Азимов обозначает триллион - число 1012, то есть Т-281 ± равно 1012’ 281,25 = 103375 ~ 211211’ . ] В ноябре 2001 г. было найдено 39-е простое число Мерсенна - 213466917 - 1 http://www.mersenne.org - Прим, перев.
238 Криптография на Си и C++ в действии простоту?», «Как можно определить, что натуральное число не яв- ляется простым (то есть составное)?», «Как разложить составное число на простые множители?». Математики веками пытались их решить, и многие вопросы до сих пор остаются без ответа. 2300 лет назад Евклид доказал, что простых чисел бесконечно мно- го (см., например, [Bund], стр. 5, особенно доказательства: забавное и серьезное, на стр. 39 и 40). Сформулируем еще одно важное утверждение, которое до сих пор подразумевалось по умолчанию: согласно основной теореме арифметики, каждое натуральное число, большее 1, можно разложить в произведение конечного числа про- стых чисел, причем это разложение единственно с точностью до порядка сомножителей. Простые числа - это своего рода «кирпи- чики» в здании натуральных чисел. Не выходя за границы множества натуральных чисел и не отвлека- ясь на слишком большие числа, мы можем эмпирически ответить на ряд вопросов и получить конкретные результаты. Конечно, эти результаты в значительной степени зависят от имеющихся вычис- лительных мощностей и эффективности используемых алгоритмов. Посмотрим на опубликованный в Интернете список самых больших простых чисел - размеры чисел поистине впечатляющие (см. таб- лицу 10.1)! Таблииа 10.1. Самые большие Простое число Число разрядов Автор Год известные простые числа (данные на май 26972593 _ j 2098960 Hajratwala, Woltman, Kurowski, GIMPS 1999 2001 г.) 23021377 _ 1 909526 Clarkson, Woltman, Kurowski, GIMPS 1998 ^2976221 _ 1 895932 Spence, Woltman, GIMPS 1997 21398269 _ 1 420921 Armengaud, Woltman, GIMPS 1996 ^1257787 _ 1 378632 Slowinski, Gage 1996 4859465536 + 1 307140 Scott, Gallot 2000 2859433 _ 258716 Slowinski, Gage 1994 2756839 _ 1 227832 Slowinski, Gage 1992 66 7071.2667071 -1 200815 Toplic, Gallot 2000 104187032768 + 1 197192 Yves Gallot 2000 Самые большие известные простые числа имеют вид 2Р - 1. Эти про- стые числа называются числами Мерсенна, по имени Марина Мер- сенна (Marin Mersenne) (1588-1648), открывшего их в процессе поис-
ГЛАВА 10. Основные теоретико-числовые функции 239 л ка совершенных чисел. (Совершенным называется натуральное число, равное сумме всех своих делителей. Например, число 496 является совершенным, так как 496 = 1+ 2 + 4 + 8+16 + 31 + 62 + 124 + 248). Для любого делителя t числа р число 2' - 1 является делителем числа 2Р - 1, так как при р = ab 2Р - 1=(2а- 1)(2“(Ь~}) + 2"(Ь~2) + ... + 1). Следовательно, число 2Р - 1 может быть простым только если чис- ; ло р простое. Сам Мерсенн в 1644 г. утверждал (правда, без доказа- тельства), что для р < 257 простыми являются числа вида 2Р - 1, где р Е {2, 3, 5, 7, 13, 17, 19, 31, 67, 127, 257}. Предположение Мерсенна подтвердилось, за исключением р = 67 и р = 257, при которых число 2Р - 1 составное. Были получены результаты и для других показате- лейр (см. [Knut], п. 4.5.4, и [Bund], п. 3.2.12). Отсюда можно, казалось бы, заключить, что чисел Мерсенна бес- конечно много, однако это утверждение до сих пор не доказано (см. [Rose], п. 1.2). Интересный обзор нерешенных задач, связан- ных с простыми числами, читатель найдет в работе [Rose], глава 12. Особым вниманием простые числа стали пользоваться с появлением криптографии с открытым ключом. Как никогда раньше выросла популярность алгоритмической теории чисел и смежных направле- ний математики. Наибольший интерес вызывают проблемы про- верки чисел на простоту и разложения на простые множители. Криптографическая стойкость многих криптоалгоритмов с открытым ключом (прежде всего, системы RSA) зависит от того, насколько трудна задача разложения (в теоретико-сложностном смысле), которая, по крайней мере на сегодняшний день, не разрешима за полиномиальное время. 11 г То же, в несколько более слабой форме, можно сказать и о распо- знавании простых чисел: как формально доказать, что данное число простое? Правда, существуют тесты, определяющие (с малой веро- ятностью ошибки) простоту числа. Более того, если тест о данном числе говорит, что оно составное, то это действительно так. Сомне- ния в правильности результата теста компенсируются полиноми- альным временем работы, а вероятность «неверного положительного ответа», как мы увидим, можно сделать сколь угодно малой, стоит лишь повторить тест нужное число раз. Старинный, но все еще действенный метод получения всех простых чисел, меньших заданного натурального числа N, придуман грече- । ским философом и астрономом Эратосфеном (276-195 до н. э.; см. также [Saga]) и назван в его честь решетом Эратосфена. Обсуждение теоретико-сложностных аспектов криптографии можно найти в [HKW], глава 6, или [Schn], пп. 19.3 и 20.8, см. также многочисленные ссылки в этих работах. Рекомендуем прочи- тать также сноску на стр. 212.
240 _____________________________Криптография на Си и C++ в действу Сначала выписываем все натуральные числа от 1 до N и исклю чаем из этого списка все числа, большие 2 и кратные ему. Затем обозначаем за р первое число из оставшихся, большее текущего простого (т.е. больше двух в первый раз) и исключаем из списка числа вида р(р + 2Z), / = 0, 1, ... и т. д. Продолжаем процесс до тех пор, пока не найдем простое число, большее J~N . После заверше ния процедуры в списке остаются простые числа, меньшие либо равные N- их мы «поймали в решето». Вкратце поясним, почему же решето Эратосфена работает так, как надо. Во-первых, простейшая индукция показывает, что число, сле- дующее за простым и оставшееся в списке, само является простым поскольку иначе у него должен быть маленький простой делитель и, следовательно, это число мы должны были отбросить раньше. Поскольку мы исключаем только составные числа, мы не теряем ни одного простого числа. Во-вторых, достаточно исключить только числа, кратные тем про- стым р, которые меньше либо равны , так как если Т - наи- меньший собственный делитель числа N, то Т < <J~N . Если бы в списке осталось составное число п < V/V , то наименьший простой делитель р числа п должен был бы удовлетворять неравенству р < и мы должны были отбросить п как кратное р, а это противоречит нашему предположению. Теперь посмотрим, как мож- но реализовать решето, а для этого нам нужен программируемый алгоритм. Но сначала несколько замечаний. Поскольку, кроме 2, других четных простых чисел нет, будем проверять на простоту только нечетные числа. Вместо того чтобы выписывать нечетные числа, составим последовательность/, где 1 < i <L(W- 1)/2_1, соот- ветствующую простоте чисел 2/ + 1. Далее, пусть переменная р со- держит текущее значение 2/ + 1 элемента (воображаемого) списка нечетных чисел, а переменная л удовлетворяет соотношению 2s + 1 = р2 = (2/ + I)2, то есть 5 = 2i2 + 21. Теперь можно и сформу- лировать алгоритм (см. [Knut], п. 4.5.4, упражнение 8). Алгоритм поиска всех простых чисел, меньших либо равных натуральному числу N (решето Эратосфена) 1. Положить L <- |_(7V - l)/2j и В <-1 V?7/2 |. Для 1 < i < L положить / <— 1. Положить i <— 1, р <— 3 и s 4. 2. Если / = 0, то перейти на шаг 4. Иначе результат: р и положить к <— j. 3. При k<L положить fk<^~ 0, к к + р и повторить этот шаг. 4. Если i < В, то положить i «— i + 1, л <— 5 + 2р, р <— р + 2 и вер нуться на шаг 2. Иначе закончить алгоритм.
Файл взят с сайта www.kodges.ru, на котором есть еще много интересной литературы
ГЛАВА 10. Основные теоретико-числовые функции 241 На основе этого алгоритма напишем программу, возвращающую в качестве результата указатель на список значений типа ULONG, содержащий все простые числа, меньшие заданной границы, в порядке возрастания. Первый элемент списка - это число всех найденных простых чисел. функция: Генератор простых чисел (решето Эратосфена) Синтаксис: ULONG * genprimes (ULONG N); Вход: N (верхняя граница поиска) Возврат: Указатель на вектор значений типа ULONG, содержащий простые числа, меньшие либо равные N. (На нулевой позиции - число най- денных простых чисел). NULL при ошибке процедуры malloc(). ULONG * genprimes (ULONG N) { ULONG i, k, p, s, B, L, count; char *f; ULONG *primes; Шаг 1. Задание начальных значений переменных. Лля вычис- ления целой части квадратного корня из числа типа ULONG используется вспомогательная функция ul_iroot(), см. соответст- вующую процедуру в п. 10.3. Составные числа помечаем с помо- щью вектора f. В = (1 + uljroot (N)) » 1; L = N » 1; if (((N & 1) == 0) && (N > 0)) { -L; } if ((f = (char *) malloc ((size_t) L+1)) == NULL) { return (ULONG *) NULL;
242 Криптография на Си и C++ в действии for (i = 1; i <= L; i++) P = 3; s = 4; На шагах 2, 3, 4 собственно и реализуем решето. Переменная i соответствует численному значению числа 2i + 1. for (i = 1; i <= В; i++) { if (f[i]> , { for (k = s; к <= L; к += p) { f [k] = 0; s += p + p + 2; P+= 2; Теперь определяем число найденных простых чисел и выделяем для переменных типа ULONG поле соответствующего размера. for (count = i = 1; i <= L; i++) { count += f[i]; } if ((primes = (ULONG*)malloc ((size_t)(count+1) * sizeof (ULONG))) == NULL)
f ДАВА 10. Основные теоретико-числовые функции 243 return (ULONG*)NULL; Оцениваем поле f[J; все числа вида 2i + 1, помеченные как про- стые, храним в поле primes. Если N 2, то учитываем и число 2. for (count = i = 1; i <= L; i++) if (f[i]) primes[++count] = (i « 1) + 1; if (N <2) { primes[0] = 0; } else { primes[0] = count; primes[1] = 2; free (f); return primes; Чтобы определить, является ли число п составным, достаточно применить к нему метод пробного деления: проверить, делится ли п на все простые числа, меньшие либо равные л/и (их можно найти с помощью решета Эратосфена). Если п не делится ни на одно из этих чисел, то оно простое. Однако этот метод непрактичен: число простых чисел быстро растет с ростом п. А именно, справедлива теорема о простых числах, сформулированная А. М. Лежандром, согласно которой число п(х) простых чисел р, где 2 <р<х, асим- птотически стремится к х/1пх при х—> (см., например, [Rose],
г 244 Криптография на Си и C++ в действии • - — глава 12) 12 *. Дадим некоторые оценки для числа простых чисел меньших заданного х. В таблице 10.2 приведены и истинные значения числа л(х) простых чисел, меньших либо равных х, и .аппроксимация х/ln х. В последней клетке таблицы стоит знак вопроса - может быть, читатель сам ее заполнит? X ю2 104 * 10® ю’6 1018 10’°° х/In X 22 1 086 5 428 681 271 434 051 189 532 24 127 471 216 847 323 4 • 1097 п(х) 25 1 229 5 761 455 279 238 341 033 925 24 739 954 287 740 860 ? Таблииа 10.2. Число простых чисел для различных значений х Сложность метода пробного деления растет почти по экспоненте с ростом х. Следовательно, для проверки простоты больших чисел этот метод совершенно не применим. Позже мы увидим, что метод пробного деления играет важную вспомогательную роль в других тестах. В принципе, нам нужен такой тест, который проверял бы число на простоту, не выдавая никакой информации о его делителях. В этом нам поможет хотя бы малая теорема Ферма, согласно которой для простого числа р и всех чисел а, взаимно простых с р, справедливо сравнение ар~х = 1 mod р (см. стр. 198). На этом факте основан тест Ферма: если найдется число а, для которого выполняется либо НОД(а, п) Ф 1, либо НОД(я, n) = 1 и 1 an~x mod /?, то число п со- ставное. Для возведения в степень an~x = 1 mod п требуется <9(log3n) операций процессора. Опыт показывает, что уже после нескольких попыток составное число будет распознано. Однако тест Ферма не является универсальным и «пропускает» некоторые числа. Посмот- рим, какие именно. Следует признать, что утверждение, обратное к малой теореме Фер- ма, вообще говоря, неверно: число п, для которого НОД(д, н) = и an~x = 1 mod п, где 1 < а < п - 1, не обязательно будет простым. Существуют составные числа /г, которые проходят тест Ферма для любых а, взаимно простых с п. Это числа Кармайкла, названные по 12 Теорема о простых числах была доказана независимо в 1896 г. Жаком Адамаром и Шарлем-ЖаноМ де ла Валле Пуссеном (см. [Bund], п. 7.3). (В 1849-1852 гг. аналогичные утверждения были сформулированы П. Л. Чебышевым; в частности, Чебышев впервые доказал, что число л(*) удовлетворяет двойному неравенству 0,921-^ < л(х) < 1,06-^ . - Прим, перев.) Я
ГЛАВА 10. Основные теоретико-числовые функции 245 имени открывшего их Роберта Дениэла Кармайкла (Robert Daniel Carmichael, 1879-1967). Вот первые три из этих загадочных чисел: 561 = 3- 11- 17,1105 = 5- 13- 17,1729 = 7- 13- 19. . К Любое число Кармайкла представляет собой произведение не менее трех различных простых чисел (см. [КоЫ], глава 5). Лишь в начале 1990-х гг. было доказано, что чисел Кармайкла бесконечно много 1,:' ЙГЛК?г..~ Ы ' (см. [Bund], п. 2.3). Относительная частота, с которой встречаются числа, меньшие п и взаимно простые с п, задается формулой (10.24) ;; j<?" | . Г ' ’ s . ' А П &! /' St .. m'v t Ф(я) н-1 ’ где ф(л) - функция Эйлера (см. стр. 198), то есть доля чисел, не вза- имно простых с п, близка к 0 при больших п. Следовательно, в большинстве случаев, чтобы распознать число Кармайкла, прихо- дится много раз проходить тест Ферма. Заставляя а пробегать весь диапазон 2 < а < п - 1, мы в конце концов найдем наименьший про- стой делитель числа л, равный а. Помимо чисел Кармайкла существуют и другие нечетные состав- ные числа л, для которых найдутся натуральные а такие, что НОД(я, п) = 1 и ап~х = 1 mod п. Эти числа называются псевдопро- стыми по основанию а. Справедливости ради отметим, что лишь немногие числа являются псевдопростыми по основаниям 2 и 3, а в интервале от 1 до 25 • 109 существует всего 1770 целых чисел, псев- допростых по основаниям 2, 3, 5 и 7 одновременно (см. [Rose], п. 3.4). Но факт остается фактом - не существует общей оценки для числа решений сравнения Ферма для составных чисел. Таким обра- зом, к недостаткам теста Ферма можно отнести, во-первых, сомне- ние в том, действительно ли число, прошедшее тест, является про- стым, а во-вторых, невозможность оценить число проходов теста для распознавания составного числа. Этих недостатков лишен следующий тест, основанный на критерии Эйлера (см. п. 10.4.1): если р нечетное простое, то для всех чисел а, не кратных р, выполняется сравнение (10.25) ^(р-п/2 s mocj ? , где (у)=±1 mod р означает символ Лежандра (Якоби). По аналогии с малой теоремой Ферма, критерий проверки на простоту получаем из следующего утверждения: если для натурального числа п суще- ствует целое а такое, что НОД(я, и) = 1 и а(п~^2 = (f) mod п, то число /г, вероятно, простое. Временная сложность соответствующего теста совпадает со сложностью теста Ферма и равна <7(log3H).
246 Криптография на Си и C++ в действии — Опять же, как и для теста Ферма, существуют составные числа ц удовлетворяющие критерию Эйлера для некоторого а. Эти числа называются эйлеровыми псевдопростыми по основанию а. Например число п = 91 = 7 • 13 эйлерово псевдопростое по основаниям 9 и 1о' так как 945 = (jf)= 1 mod 91 и 1045 = (>)=-! mod 91.13 Эйлерово псевдопростое по основанию а всегда является псевдопро- стым по тому же основанию (см. стр. 245), так как, возводя почленно в квадрат сравнение а{п~х^2 = (^)mod п , получаем ап~[ = 1 mod п. К счастью, для критерия Эйлера не существует чисел, аналогичных числам Кармайкла, а следующие наблюдения, отмеченные Р. Соло- вэем (R. Solovay) и В. Штрассеном (V. Strassen), позволяют значи- тельно сузить круг составных чисел, удовлетворяющих критерию Эйлера. (а) Для составного числа п число целых чисел а, взаимно простых с для которых б7(л-1)/2 =(f)modn, не превышает уф(и) (см. [КоЫ], п. 2.2, упражнение 21). Отсюда следует, что: (б) Вероятность для составного п и случайно выбранных к чисел ..., ак, взаимно простых с и, получить а^'1^2 = (y)mod п , где 1 < г < п, не превышает 2~к. Теперь мы можем реализовать критерий Эйлера в виде вероятност- ного теста, где термин «вероятностный» означает, что результат «число п не простое» является безусловным, а говорить о том, что п является простым мы можем лишь с определенной вероятностью ошибки. Вероятностный алгоритм проверки натурального числа п на простоту (тест Соловэя-Штрассена) ' - • о, 1. Выбрать случайное число 2 < а < п - 1 такое, что НОД(я, ri) = 1- 2. Если a(n~l)^2 =(f)modn, то результат: «Число п вероятно про- PX‘Rt; '-Я f ' стое»; иначе результат: «Число п составное». Возведение в степень и вычисление символа Якоби выполняется за время <?(log3n). Многократно повторяя этот тест, можно уменьшить вероятность ошибки в смысле свойства (б). Например, при к = $ вероятность ошибки пренебрежимо мала - меньше, чем 2”60 ~ Ю Как отмечает Д. Кнут, это меньше, чем случайная аппаратная ошибка, вызванная, например, альфа-частицей, попавшей в пронес' сор или память компьютера и изменившей значение бита. 13 В кольце Z9i элемент 3 имеет порядок 9, а элемент 6 — порядок 10, поэтому 93 = 106 = 1 mod 91- Поэтому 945 — 93 15 = 1 mod 91 и 1045 = 106 7+3 = Ю3 =-1 mod 91.
ГЛАВА 10. Основные теоретико-числовые функции 247 Казалось бы, мы должны быть довольны этим тестом: мы можем контролировать вероятность ошибки и у нас есть эффективные ал- горитмы для выполнения всех необходимых вычислений. Однако существует ряд результатов, позволяющих получить более мощный тест. Чтобы лучше понять суть наиболее широко используемых на сегодняшний день тестов, проведем некоторые рассуждения. Предположим, что число п простое. Тогда по малой теореме Ферма для всех целых чисел а, не кратных л, имеем: я""1 = 1 mod п. Квад- ратный корень из an~l mod п может принимать лишь значения 1 и -1, поскольку это единственные решения сравнения х2 = 1 mod п (см. п. 10.4.1). Вычислим последовательно один за другим квадрат- ные корни а(п~^2 mod п, а(п~^4 mod и, ..., а(п~^2 mod и, пока не получим нечетное число (п - 1)/2г. И если на некотором шаге мы получим вычет, не равный 1, то он должен быть равен -1 (иначе п не может быть простым, что противоречит нашему пред- положению). Если же квадратный корень, предшествующий 1, будет равен -1, то мы можем по-прежнему верить, что число п яв- ; ляется простым. Составные числа п, обладающие таким свойством, называются сильными псевдопростыми по основанию а. Сильное псевдопростое число по основанию а всегда является эйлеровым псевдопростым по тому же основанию (см. [КоЫ], глава 5). ‘ . Сведем полученные результаты в следующий вероятностный тест. Из соображений эффективности сначала вычислим b = а{п~х^2 mod п с г нечетным показателем (п - 1)/2', и если b * 1, будем возводить b в квадрат, пока либо не получим ±1, либо не достигнем д(л-1)/2 mod п. Во втором случае либо b должно быть равно -1, либо число п со- ш ставное. Укороченный вариант алгоритма, без выполнения послед- него возведения в квадрат, взят из книги [Cohe], п. 8.2. Вероятностный алгоритм проверки нечетного числа n > 1 на простоту (тест Миллера-Рабина) 1. Определить q nt, где п - 1 = 2 g, число q нечетное. 2. Выбрать случайное число а из интервала 1 < а < п. Положить е <— 0, b <г- a1 mod п, При b = 1 завершить алгоритм с результа- том «Число п вероятно простое». 3. Пока b £ 1 mod п и е < t - 1, вычислять b b2 mod п и е <r- е + 1. Если теперь b п - 1, то результат: «Число п состав- ное»; иначе результат: «Число п вероятно простое». Время возведения в степень равно <9(log3/z), поэтому сложность теста Миллера-Рабина (или, короче, MP-теста) та же, что и слож- ность теста Соловэя-Штрассена.
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). Прежде чем перейти к реализации теста Миллера-Рабина, укажем два способа улучшения алгоритма. Во-первых, разумнее сначала применить к тестируемому числу ме- тод пробного деления, то есть проверить, не делится ли это число на маленькие простые числа. Если делитель будет найден, то нечего и применять тест Миллера-Рабина. Но тогда сразу встает вопрос: сколько нужно таких маленьких простых чисел, прежде чем мы сможем применить MP-тест? Воспользуемся рекомендациями А. К. Ленстры: наибольший эффект достигается, если проверить делимость на 303 простых числа, меньших 2000 (см. [Schn], п. Н-Я Откуда взялись такие цифры, становится ясно, если заметить, что относительная частота появления нечетных чисел, которые не кратны простым числам, меньшим п, примерно равна 1,Г2/1пи- Проверяя делимость на простые числа, меньшие 2000, мы отбрИ сываем 85% составных чисел без всякого теста Миллера-Рабина, а его используем лишь для проверки оставшейся доли чисел. Проверка делимости на маленькое простое число требует лиШЧ О(1п п) элементарных операций. Для этого в методе пробного делв1 ния будем использовать специальную процедуру, особенно эфФеК1 тивную при делении на маленькие числа. |
ГЛАВА 10. Основные теоретико-числовые функции 249 Реализуем метод пробного деления в виде функции sieve_l(), кото- рая, в свою очередь, использует простые числа, меньшие 65536, для хранения которых выделим поле smallprimes[NOOFSMALLPRIMES]. Простые числа будем хранить в виде разностей, то есть под каждое простое число требуется лишь один байт памяти. Ограниченный доступ к этим числам не создает особых проблем, поскольку мы рассматриваем эти числа в естественном порядке. Особо следует выделить случай, когда само тестируемое число является малень- ким простым и содержится в таблице. Во-вторых, в тесте Миллера-Рабина будем использовать не слу- чайные основания, а маленькие простые числа 2, 3, 5, 7, 11, ... < В. Это значительно ускоряет работу функции возведения в степень (см. главу 6) и, как показывает опыт, ничуть не ухудшает результа- ты теста. И вот, наконец, метод пробного деления. Процедура деления на маленькие числа реализована в виде функции divj(). Функция: Метод пробного деления Синтаксис: USHORTsieveJ(CLINT aj, unsigned no_of_smallprimes); Вход: а_1 (тестируемое число) no_of_smallprimes (число простых чисел, на которые мы делим, без учета числа 2) Возврат: Простой делитель, если таковой найден 1, если тестируемое число само является простым 0, если делитель не найден USHORT sievej (CLINT aj, unsigned int no_of_smallprimes) { clint *aptrj; USHORT bv, rv, qv; ULONG rdach; unsigned int i = 1; I Для полноты картины сначала проверяем, не является ли а_1 кратным 2. Если а_1 равно 2, то возвращаем 1; если же а_! четное, I в большее 2, то возвращаем 2 в качестве делителя. if (ISEVEN_L (aj)) { if (equj (aj, two J))
250 Криптография на Си и C++ в действии • У, { return 1; } else { return 2; } } bv = 2; do { Определяем простые делители, последовательно суммируя числа из smallprimes[], сумму записываем в bv. Первое простое число, проверяемое в качестве делителя, - это 3. Для деления на число типа USHORT используем код из соответствующей быстрой про- граммы (см. п. 4.3). rv = O; bv += smallprimes[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 само простое!), то он и будет результатом. Если а_1 само является простым, то результат: 1; в противном случае результат: 0. if (0 == rv) if (DIGITSJ- (aJ) == 1 && *LSDPTRJ_ (aj) == bv)
251 ГЛАВА 10. Основные теоретико-числовые функции bv = 1; } /* else: Результат в bv является простым делителем числа aj 7 } else /* Делитель числа aj не найден 7 { bv = 0; } return bv; } С помощью функции sieveJO можно находить простые делители, меньшие 65536, объектов типа CLINT. Для этого в flint.h имеется макрос SFACTOR_L(n_l), вызывающий функцию sievej(nj, NOOFSMALLPRIMES), которая, в свою очередь, проверяет, делится ли nJ на простые числа из базы smallprimes[]. Макрос SFAC- TORJ-(nJ) возвращает то же значение, что и функция sievej. Многократно вызывая макрос SFACTOR_L(nJ) и тем самым после- довательно определяя простые делители, мы можем найти полное разложение чисел, меньших 232, то есть представимых стандартны- ми целочисленными типами. И вот уже primeJO - вполне созревшая функция, объединяющая в себе метод пробного деления и тест Миллера-Рабина. Для прида- ния ей большей гибкости будем рассматривать число простых де- лителей в предварительном пробном делении и число проходов теста Миллера-Рабина как параметры. В прикладных задачах для простоты можно использовать макрос ISPRIME_L(CLINT nJ), кото- рый, в свою очередь, вызывает функцию primeJO с наперед задан- ными параметрами. Возведение в степень будем осуществлять с помощью функции wmexpmj(), сочетающей в себе всю прелесть приведения по Монтгомери и малых оснований степени (см. главу 6). Функция: Вероятностный тест Миллера-Рабина с использованием метода пробного деления Синтаксис: int primej (CLINT nJ, unsigned no_of_smallprimes, unsigned iterations); ^Х°Д«* nJ (тестируемое число) no_of_smallprimes (число простых чисел в методе пробного деления) iterations (число итераций теста) возврат: 1, если тестируемое число «вероятно» простое 0, если тестируемое число составное или равно 1
252 Криптография на Си и C++ в действии int primej (CLINT nJ, unsigned int no_of_smallprimes, unsigned int iterations) { CLINT dj, xj, qj; < USHORT i, j, k, p; t int isprime = 1; J if (EQONE.L (nJ)) { return 0; } Теперь выполняем пробное деление. Если делитель найден, то функция завершается с результатом 0. Если функция sieveJ0 вы- дала 1 (это означает, что число nJ простое), то функция завер- шается с результатом 1. В противном случае выполняем тест Миллера-Рабина. k = sievej (nJ, no_of_smallprimes); if (1 == к) return 1; } if (1 < к) { return 0; } else Шаг 1. Используя функцию twofactJO, находим разложение n - 1 = 2kq, где число q нечетное. Значение п - 1 записываем в dj-
253 ГЛАВА 10. Основные теоретико-числовые функции cpyj (dj, nJ); dec J (dj); k = (USHORT)twofactJ (dj, qj); |L isprime = 1; do { Шаг 2. Из прирашений, хранящихся в поле smallprimes[], форми- руем основания р. Для возведения в степень используем функ- цию Монтгомери wmexpmj, поскольку основание всегда будет иметь тип USHORT и, кроме того, после предварительной про- верки методом пробного деления числа nJ, всегда будет нечет- ным. Если в результате степень (xj) равна 1, то переходим к сле- дующей итерации. р += smallprimes[i++]; wmexpmj (р, qj, xj, nJ); if (!EQONE_L (xj)) hO'. { I Шаг 3. Пока xj отлично от ±1 и пока число итераций не превы- шает к - 1, выполняем возведение в квадрат. hL while (!EQONE_L (xj) && !equj (xj, dj) && ++j < k) { ' ‘ msqrj (xj, xj, nJ); } if (!equj (xj, dj)) { 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, Н. Cohen, А. К. Lenstra), опубликованным в 1981 г. X. Ризель (Н. Riesel) назвал этот тест прорывом, доказавшим, что быстрые универсальные достоверные алгоритмы проверки на простоту дей- ствительно существуют (см. [Ries], стр. 131). Этот тест распознает простоту числа п за время <9((1п п)с1п,п1п") для некоторой подходя- щей константы С. Поскольку на практике показатель In In In п ведет себя как константа, этот тест можно считать полиномиальным. Теперь можно проверять на простоту целые числа длиной в не- сколько сотен десятичных знаков за такое время, которое раньше могли показать только вероятностные тесты. 15 Алгоритм, исполь- зующий аналог малой теоремы Ферма для более сложных алгеб- раических структур, довольно сложен теоретически и труден в реа- 14 В статье «Генерация вероятно простых случайных чисел» (Р. Beauchemin, G. Brassard, С. Сгёреаа» С. Goutier и С. Pomerance, Journal of Cryptology, Vol. 1, No. I, 1988) говорится о том, что утвер ждение Д. Кнута верно лишь потому, что вероятность ошибки для большинства составных ни сел гораздо меньше 14. В противном случае оценка, данная Кнутом, будет значительно больше, чем 1(Г6. 15 Коэн (Cohen) в этой связи замечает, что, хотя практический вариант алгоритма APRCL также ве роятностный, существует и менее практичная, детерминированная его версия (см. [Cohe], глава 9)-
ГЛАВА 10. Основные теоретико-числовые функции 255 лизации. Более подробную информацию см. в [Cohe], глава 9, в [Ries] или в указанной выше оригинальной статье. Читатель может спросить, получим ли мы достоверно простое число, применяя тест Миллера-Рабина для большого числа оснований. Согласно результату Г. Миллера (G. Miller), нечетное натуральное число п является простым тогда и только тогда, когда тест Мил- лера-Рабина определяет его как простое для всех оснований а таких, что 1 < а < С • 1п2л (в [КоЫ], п. 5.2, указано, что С- 2), в пред- положении, что верна расширенная гипотеза Римана (см. стр. 222). При таких условиях тест Миллера-Рабина является детерминиро- ванным и полиномиальным и за 2,5 • 105 итераций дает достовер- ный результат для чисел длины 512 бит. Если на каждую итерацию отводится 10”1 секунд (время возведения в степень на быстрых ПК; см. Приложение D), тогда достоверный тест займет около семи часов. Однако, принимая во внимание, что этот тест основан на не- доказанной гипотезе, а также учитывая довольно длительные вы- числения, можно ожидать, что такой теоретический результат не устроит ни «чистых» математиков, ни программистов-прагматиков, любящих быстрые программы. Генри Коэн, отвечая на процитированный выше вопрос Д. Кнута, был категоричен ([Cohe], п. 8.2): «Все же проверка на простоту тре- бует строгих математических доказательств».
ГЛАВА И. Большие случайные числа Математика полна псевдослучайности, которой вполне хватит всем изобретателям-мечтателям на все времена. Д.Р. Хофштадтер, Гедель, Эшер, Бах Последовательности «случайных» чисел широко используются в статистических процедурах, в вычислительной математике, в физике, а также в теоретико-числовых приложениях, когда нужно либо за- менить ими статистические наблюдения, либо автоматизировать процесс ввода каких-то переменных величин. Случайные числа могут пригодиться: ✓ если мы хотим выбрать несколько случайных элементов из боль- шого множества; ✓ в криптографии для генерации ключей и работы защищенных про- токолов; ✓ в качестве начальных значений при генерации простых чисел; ✓ для тестирования компьютерных программ (к этой теме мы еще вернемся); для развлечения и много для чего еще. При компьютерном моделировании естест- венных процессов случайными числами можно представлять из- меряемые величины (методы Монте-Карло). Случайные числа полезны и в том случае, если нам нужны произвольные, случайным образом выбранные, числа. Прежде чем приступить в этой главе к разработке каких-либо функций генерации больших случайных чи- сел, которые нам понадобятся, в частности, для криптографических приложений, проведем некоторую методологическую подготовку. Существует множество способов получения случайных чисел, однако мы сразу условимся разделять истинно случайные числа, возникаю- щие в результате случайных экспериментов, и псевдослучайные числа, выработанные алгоритмически. Истинно случайные числа можно получить, подбрасывая монету или кость, вращая («честное») колесо рулетки, наблюдая процесс радиоактивного распада на специальном измерительном оборудовании. Напротив, псевдослучайные числа вы- рабатываются алгоритмами, с помощью генераторов псевдослучайных чисел, которые, в свою очередь, являются детерминированными и, вследствие этого, предсказуемыми и воспроизводимыми. Таким образом, псевдослучайные числа не являются случайными в строгом смысле слова. Этим обстоятельством, однако, можно пренебречь, если у нас есть алгоритмы, производящие «высококачественные» случайные числа. Поясним, что мы понимаем под этим словом. -1697
258 Криптография на Си и C++ в действии (11.1) Прежде всего, обратим внимание читателя на то, что бессмысленно говорить о «случайности» какого-то одного числа. Математические критерии случайности всегда применяются к последовательности чисел. Д. Кнут говорит о последовательности независимых случайных чисел с определенным законом распределения, в которой каждый элемент вырабатывается случайно и независимо от всех других членов последовательности и принимает значение из некоторого диапазона с определенной вероятностью (см. [Knut], п. 3.1). Слова «случайно» и «независимо» используются здесь, чтобы подчеркнуть, что харак- тер и способ взаимодействия событий, определяющих выбор кон- кретного числа, слишком сложен, чтобы распознать его статисти- ческими или какими-либо другими тестами. Теоретически достичь этого идеала детерминированными процеду- рами невозможно. Цель же многочисленных алгоритмических средств генерации чисел - как можно ближе приблизиться к этому идеалу. Параллельно разрабатываются теоретические и эмпириче- ские тесты для распознавания характера и структуры последова- тельностей псевдослучайных чисел и, следовательно, для оценки качества алгоритмов генерации этих последовательностей. Не бу- дем слишком этим увлекаться: теория здесь слишком глубока и сложна. Хороший обзор на эту тему желающие смогут найти в книге [Knut], а исчерпывающие теоретические оценки генераторов слу- чайных чисел - в работе [Nied], Ряд прагматических идей по тести- рованию последовательностей случайных чисел есть в [FIPS]. Из множества существующих способов генерации псевдослучайных чисел (для краткости будем иногда опускать слово «псевдо» и гово- рить просто «случайные числа», «случайные последовательности» и «генераторы случайных чисел») уделим сначала немного времени проверенному и часто используемому методу генерации линейных конгруэнтных последовательностей. По заданному начальному значению Хо элементы последовательности определяются из линей- ного рекуррентного соотношения: Х;+1 = (Хр + b) mod m. Эта процедура была предложена Д. Лемером (D. Lehmer) в 1951 году и с тех пор завоевала большую популярность. Несмотря на кажущуюся простоту, линейные конгруэнтные последовательности обладают хорошими свойствами случайности. Их качество, как. нетрудно догадаться, зависит от выбора параметров а, b и т. В книге [Knut] показано, что линейная конгруэнтная последовательность с тщательно подобранными параметрами проходит испытания стати- стическими тестами «на ура», однако случайный выбор параметров почти всегда приводит к плачевным результатам. Мораль сей басни такова: при выборе параметров будьте осторожны! Выбор в качестве т степени двойки обладает очевидным преимУ' ществом: вычислять вычет по модулю т можно с помощью мате-
ГЛАВА 11. Большие случайные числа 259 магической операции AND. Но, как всегда, есть и недостаток - младшие двоичные разряды генерируемых чисел характеризуются гораздо меньшей случайностью, чем старшие, а значит, надо быть очень аккуратным при работе с такими числами. Да и вообще, числа, полученные из линейной конгруэнтной последовательности приведением по модулю простого делителя числа т, проявляют весьма посредственные свойства случайности, поэтому следует рассмотреть возможность выбора в качестве т простого числа, так как в этом случае любые двоичные разряды ничуть не хуже, чем любые другие. Выбор чисел а и т влияет на периодичность последовательности: поскольку элементы последовательности могут принимать конеч- ное число, а именно т, различных значений, последовательность начнет повторяться самое позднее на (пг + 1)-м элементе. То есть, эта последовательность периодическая. (Говорят также, что после- довательность входит в цикл). Точка входа в цикл - не обязательно начальное значение Хо, это может быть и некоторое более позднее значение Хц. Числа Хо, Х2, ...» Х^. образуют «хвост» последова- тельности. Поведение такой последовательности схематично изо- бражено на рис. 11.1. Рисунок 11.1. у Повеление . ~ ~ «Хвост» Пикл псевдослучайной «лыдл» последова- f \ тельности I I I V Поскольку повторение чисел с коротким периодом совершенно г. . ( не подходит ни под какие критерии, мы должны приложить все а. п усилия, чтобы максимально увеличить длину цикла или даже построить генератор, вырабатывающий только последовательности с максимальной длиной цикла. Сформулируем критерий, позволяю- г’ щий создавать именно такие линейные конгруэнтные последова- тельности с параметрами a, b и т. Итак, должны выполняться сле- дующие условия: (а) НОД(/>,ш)=1. (б) Если р | т, то р | (а - 1) для любого простого числа р. (в) Если 4 | т, то 4 | (а-1). Доказательство и дополнительные подробности см. в [Knut], п. 3.2.1.2. Стандарт ISO-С рекомендует использовать для функции rand() сле- дующую линейную конгруэнтную последовательность, параметры которой удовлетворяют указанному критерию: Xi+i = (Xi -1103515245 + 12345) mod m, 9*
г 260 Криптография на Си и C++ в действии где т = 2к, где к выбирается так, чтобы 2к ~ 1 было наибольшим числом, которое можно задать типом unsigned int. Значением функ-, ции rand() является не Х/+ь a X/+i/216 mod (RAND_MAX +1), то есть все значения функции rand() заключены между 0 и RAND_MAX. Макрос RAND_MAX определен в файле stdio.h и должен быть по крайней мере не меньше, чем 32267 (см. [Plal], стр. 337). Здесь,I очевидно, учтены рекомендации Д. Кнута обходиться без младших двоичных разрядов, когда модуль равен степени двойки. Легко проверить, что условия (а)-(в) выполнены, а значит, указанная по- следовательность имеет максимально возможный период длины 2к. Удовлетворяет ли указанным условиям конкретная реализация на языке С, исходный текст которой, как правило, неизвестен,1 можно при благоприятных условиях с помощью следующего алгоритма Р. П. Брента. Этот алгоритм вычисляет длину X периода последова- тельности, вычисленной по рекурсивной формуле X/+i = F(XZ) с по- мощью порождающей функции F : D —» D из начального значения Хо G D. Для этого требуется не более 2 • тах{ц, X} раз вычислить функцию F (см. [HKW], п. 4.2). Алгоритм Брента определения длины X периода последовательности вида XI+1 = F(XZ) с начальным элементом Хо 1. Положить у <— Хо, г <- 1 и к <- 0. 2. Положить х <г- у, j <— к и г <— г + г. 3. Положить к <— к + 1 и у <- F(y). Повторять этот шаг, пока не по- лучится х = у или к>г. 4. Еслих Ф у, то вернуться на шаг 2. Иначе результат: X = k-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 ^(H(Xl)-nP(Xi))2 (11.3) IU : Я? • > где для t различных событий Xt через H(XZ) обозначена наблю- даемая частота события Хь через Р(Х;) - вероятность появления события X,-, а п - это объем выборки. Для распределения, соответст- вующего указанному, математическое ожидание статистики %2, рассматриваемой как случайная величина, равно Е(%2) = г-1. Пороговые значения, при которых мы отвергаем гипотезу о равен- стве распределений с заданной вероятностью ошибки, можно опре- делить по таблицам хи-квадрат распределения с t - 1 степенями свободы (см. [Bosl], п. 4.1). Проверка критерия хи-квадрат применяется для проверки соответ- ствия результатов многих эмпирических тестов теоретически вы- численным распределениям. Особенно легко применять критерий хи-квадрат к последовательностям равномерно распределенных (это и есть гипотеза теста!) случайных чисел Xt из диапазона значе- ний {0, w- 1}. Мы считаем, что каждое из чисел множества W может быть взято с одной и той же вероятностью р = 1Лг, и таким образом предполагаем, что среди п случайных чисел Xt каж- дое число из W встречается примерно nlw раз (мы считаем п > w). Однако это не обязательно так, поскольку вероятность Рк того, что среди п случайных чисел X,- заданное значение w е W появится в точности к раз, вычисляется как рк = скрк(1 -Ргк = Рк(1 -ру-к 2 ; к\(п-к)\ (11.4) Это биномиальное распределение действительно принимает наи- большее значение при к ~ n/w, но вероятности Ро = (1 ~р)п и ?п = рп не равны нулю. Следовательно, в предположении, что последо- вательность Xi ведет себя как случайная, мы можем ожидать, что частоты hw отдельных значений we W будут распределены по биномиальному закону. Так ли это на самом деле, проверяется по критерию хи-квадрат: Г ;Г /=о п i=Q Проверка повторяется для нескольких случайных выборок (отрезков последовательности Xz). Грубая аппроксимация ^-распределения по- зволяет нам сделать вывод, что в большинстве случаев результат Ск — биномиальный коэффициент. — Прим. ред.
262 Криптография на Си и C++ в действии должен лежать в интервале [w-2y/w, vv + 2Vw]. В противном слу- чае данную последовательность можно считать недостаточно слу- чайной. Отсюда следует, что вероятность ошибки, то есть вероят- ность признать действительно «хорошую» последовательность «плохой» на основании результатов теста хи-квадрат, равна при- мерно 2%. Подчеркнем, что этот тест корректен только для доста- точно большого числа выборок: это число должно быть по крайней мере равно п = 5w (см. [Bos2], п. 6.1), а в идеале - как можно больше. Линейный конгруэнтный генератор из стандарта ISO-С, который мы рассматривали выше, проходит этот простой тест, как и другие генераторы псевдослучайных чисел, которые нам еще предстоит реализовать в пакете FLINT/C. После такого краткого экскурса в статистику вспомним о том, что случайные последовательности должны удовлетворять, помимо статистических, и другим критериям, в зависимости от области применения. Случайные последовательности, используемые для криптографических приложений, должны быть такими, чтобы без знания некоторой дополнительной (секретной) информации их невозможно было предсказать или восстановить по нескольким заданным элементам. То есть нарушитель не должен иметь воз- можности воспроизвести криптографический ключ или последова- тельность ключей, вырабатываемых с помощью псевдослучайной последовательности. Хорошо зарекомендовал себя в этом смысле генератор BBS Л. Блюм (L. Blum), М. Блюма (М. Blum) и М. Шуба (М. Shub), основанный на результатах теории сложности. Сначала опишем, а затем реализуем этот генератор, при этом не будем углубляться в теорию (читатель может с ней ознакомиться в [Blum] или [HKW], глава 4 и п. 6.5). Нам понадобятся два простых числа р и q такие, что р = q = 3 mod 4, их произведение - число п, а также число X взаимно простое с п. Вычислив Xq := X2 mod п, получаем начальный элемент Хо п0‘ следовательности чисел, получаемых рекуррентным возведением в квадрат: X/+i = X,2 mod п . В качестве случайного числа берем младший бит элемента X/. Полученная таким образом случайная последовательность битов является безопасной с точки зрения криптографии: предсказать следующий бит из уже вычисленных можно только зная делители/? и q числа л. Если же эти два числа хранятся в секрете, то для пред- сказания последующих битов с вероятностью, большей у, или ДДЯ восстановления неизвестных отрезков последовательности нужно разложить на множители число и. В основе безопасности генератора BBS лежат те же принципы, что и в криптосистеме RSA. За доверие к генератору BBS приходится заплатить трудностью вычисления
ГЛАВА 11. Большие случайные числа 263 случайных битов: для каждого бита нужно возводить в квадрат по модулю большого целого числа, чем и обусловлено длительное время генерации больших случайных последовательностей. Если нужны короткие последовательности случайных чисел, например при генерации отдельных криптографических ключей, это обстоя- тельство не столь важно. Здесь играет роль лишь вопрос безопасно- сти, хотя при ее оценке следует учитывать и процесс получения на- чальных значений. Генератор BBS детерминированный, поэтому «чистая случайность» может быть достигнута только при тщатель- ном выборе начального значения. Для этого можно использовать дату или время, статистические характеристики системы (напри- мер, число «тиков» системных часов при выполнении заданного процесса), числовые характеристики внешний событий, таких как время между нажатием клавиши клавиатуры или мыши, и многие другие методы, которые лучше всего сочетать друг с другом (сове- ты по выработке начальных значений читатель найдет в работах [East] и [Matt]).3 Теперь вернемся к теме, которой и посвящена эта глава, и на имеющейся базе построим два генератора случайных чисел формата CLINT. В качестве отправной точки на пути к генерации простых чисел научимся, например, создавать большие числа заданной дво- ичной длины. Для этого старший бит полагаем равным 1, а осталь- ные биты генерируем случайным образом. Сперва построим линейный конгруэнтный генератор и из элемен- тов полученной последовательности будем выбирать разряды слу- чайного числа типа CLINT. Параметры а = 6364136223846793005 и т = 264 для нашего генератора возьмем из таблицы результатов спектрального теста (см. [Knut], стр. 102-104). Тогда последова- тельность Xf+i = (X,• • а + 1) mod т будет иметь максимальную длину периода X = т и обладать хорошими статистическими свойствами, что можно заключить на основании результатов, представленных в таблице. Реализуем генератор в виде функции rand64J. При каждом вызове функции rand64J очередной элемент последовательности генерируется и записывается в глобальный CLINT-объект SEED64, объявленный как static. Параметр а хранится в глобальной пере- менной А64. Функция возвращает указатель на SEED64. Функция: Линейный конгруэнтный генератор с периодом 264 Синтаксис: clint * rand64J (void); Возврат: Указатель на SEED64 с полученным случайным числом В критических приложениях для генерации начальных значений или всей случайной последова- тельности всегда следует использовать истинно случайные числа, полученные с помощью под- ходящих аппаратных компонентов.
264 Криптография на Си и C++ в действии clint * rand64J (void) { mulj (SEED64, A64, SEED64); incj (SEED64); ( J Для приведения по модулю 264 просто устанавливаем нужную дли- ну поля в SEED64, что не требует почти никаких временных затрат. SETDIGITS-L (SEED64, MIN (DIGITS.L (SEED64), 4)); return ((clint *)SEED64); } Теперь нам нужна функция, задающая начальные значения для rand64J(). Назовем ее seed64J(). На вход этой функции поступает переменная типа CLINT, из не более чем четырех старших разрядов которой берется начальное значение переменной SEED64. Преды- дущее значение переменной SEED64 копируется в статическую пе- ременную BUFF64 типа CLINT, а возвращает эта функция указатель на BUFF64. Функция: Задание начальных значений для функции rand64J() Синтаксис: clint * seed64J (CLINT seedj); Вход: seedj (начальное значение) Возврат: Указатель на переменную BUFF64, содержащую предыдущее зна- чение переменной SEED64 clint * seed64J (CLINT seedj) { int i; cpyj (BUFF64, SEED64); for (i = 0; i <= MIN (DIGITS_L (seedj), 4); i++) { SEED64[i] = seedj[i];
ГЛАВА 11. Большие случайные числа return BUFF64; } Еще один вариант функции seed64J() с аргументом типа ULONG Функция: Задание начальных значений для функции rand64J() Синтаксис: clint * ulseed64J (ULONG seed); Вход: seed (начальное значение) Возврат: Указатель на переменную BUFF64, содержащую предыдущее зна_ чение переменной SEED64 clint * ulseed64J (ULONG seed) { cpyj (BUFF64, SEED64); ul2clintj (SEED64, seed); return BUFF64; } «)И Следующая функция возвращает случайные числа типа ULONG. В процессе генерации каждого числа происходит обращение к функции rand64J(), при этом для построения числа нужного типа используются старшие разряды переменной SEED64. Функция: Синтаксис: Возврат: генерация случайного числа типа unsigned long unsigned long ulrand64J (void); Случайное число типа unsigned long ULONG ulrand64J (void) { ULONG val; USHORT 1; rand64J(); 1 = DIGITS-L (SEED64); switch (1) {
266 Криптография на Си и C++ в действии case 4: case 3: case 2: val = (ULONG)SEED64[I-1]; val += ((ULONG)SEED64[I] « BITPERDGT); break; case 1: val = (ULONG)SEED64[I]; break; default: val = 0; } return val; } В пакете FLINT/C есть дополнительные функции ucrand64J(void) и usrand64J(void) для генерации случайных чисел типа UCHAR и USHORT соответственно. Здесь мы их обсуждать не будем, а рас- смотрим лучше функцию ranf_l(), вырабатывающую случайные числа типа CLINT с заданным числом двоичных разрядов. Функция: Генерация случайного числа типа CLINT Синтаксис: void randj (CLINT rj, int I); Вход: I (число двоичных разрядов - длина генерируемого числа) Выход: r l (случайное число из интервала 21"1 < r l < 21 - 1) void randj (CLINT rj, int I) { USHORT i, j, Is, Ir; Сначала ограничиваем число двоичных разрядов I максимально допустимым значением для типа CLINT. Затем определяем тре- буемое число разрядов типа USHORT (Is) и позицию (Ir) старшего двоичного разряда в старшем USHORT-разряде.
ГЛАВА 11. Большие случайные числа 267 I = MIN (I, CLINTMAXBIT); Is = (USHORT)I » LDBITPERDGT; lr = (USHORT)I & (BITPERDGT - 1UL); aV’l ХГ Теперь последовательно генерируем разряды числа rj, каждый раз вызывая функцию usrand64_l(). Таким образом, младшие двоичные разряды числа SEED64 при построении CLINT-разрядов не используются. for (i = 1; I <= Is; i++) rj[i] = usrand64J (); Далее идет «ювелирная обработка» значения r_l - задание стар- шего бита. Если lr > 0, то бит на (1г-1)-й позиции (Is + 1 )-го USHORT-разряда полагаем равными 1, а все более старшие - равными 0. Если же lr = 0, то ставим 1 в самый старший бит USHORT-разряда с номером Is. if (lr > 0) rj[++ls] = usrand64J (); j = 1U « (lr- 1); /* j <- 2A(lr- 1) */ r_l[ls] = (r_l[ls]|j)&((j«1)-1); else r_l[ls] |= BASEDIV2; SETDIGITSJ_ (rj, Is); И завершим эту главу реализацией генератора BBS. Для этого с помощью функции primeJO найдем простые числа р и q такие, что р = q == 3 mod 4 примерно с одним и тем же числом двоичных раз- рядов (это нужно для того, чтобы разложить модуль на множители было максимально трудно, на чем, собственно, и основана крипто- графическая стойкость генератора BBS, см. стр. 363). Из этих чисел
268 Криптография на Си и C++ в действии t J > г.л - w. составляем модуль п = pq. Модуль такого вида длиной 2048 бит читатель найдет в пакете FLINT/C, хотя числа р и q там не указаны (их знает только автор). В static-переменные XBBS и MODBBS будем записывать соответст- венно элементы последовательности Xz и модуль п. Из них функция randbit() вычисляет случайный бит следующим образом. " 41 < Функция: Синтаксис:* * Возврат: Псевдослучайный генератор Блюм-Блюма-Шуба int randbitj (void); Элемент множества {0, 1} ; ЛТЛЭ ’ ( О*' J 1 Й . > . i WW. « ‘ ‘ » Й? , static CLINT XBBS, MODBBS; static const char *MODBBSSTR = "81 aa5c..."; /* Модуль как строка символов */ int randbitj (void) { msqrj (XBBS, XBBS, MODBBS); с В качестве результата берем младший бит числа XBBS. return (*LSDPTR_L (XBBS) & 1); } Для инициализации генератора BBS воспользуемся функцией seedBBSJ(). Функция: Синтаксис: Вход: Задание начальных значений для функций randbitJO и randBBSJO int seedBBSJ (CLINT seedj); seedj (начальное значение) int seedBBSJ (CLINT seed J) CLINT gcdj;
ГЛАВА 11. Большие случайные числа 269 str2clint_l (MODBBS, (char JMODBBSSTR, 16); gcdj (seedj, MODBBS, gcdj); if (IEQONEJ. (gcdj)) { return-1; i ) msqrj (seedj, XBBS, MODBBS); return 0; } Функция ulrandBBS_l(), которую тоже можно использовать для генерации случайных чисел типа 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) + randbitJO; } return r; } Нам не хватает еще функции randBBS_l(CLINT г J, int I), генери- рующей случайные числа r_l длины ровно I двоичных разрядов, то есть r_l из интервала 21"1 < r_l < 21 - 1. Мы не приводим здесь ее описание, поскольку она во многом совпадает с функцией rand_l(). Но разумеется, эту функцию читатель найдет в пакете FLINT/C.
ГЛАВА 12. Стратегия тестирования LINT Не обвиняйте компилятор. Дэвид А. Спулер, C++ и С: Отладка, тестирование и достоверность кода В предыдущих главах не раз упоминалось о тестировании отдель- ных функций. Без проведения осмысленных тестов, удостоверяю- щих качество нашего пакета, вся проделанная нами работа оказа- лась бы бесполезной, ибо чем ещё можно обосновать нашу уверен- ность в надежности разработанных функций? Поэтому сейчас мы собираемся посвятить всё внимание этой важной теме, и с этой целью поставим перед собой два вопроса, которыми должен зада- ' ваться каждый разработчик программного обеспечения: ✓ Как удостовериться, что функции нашего программного обеспече- ния ведут себя в соответствии с их спецификацией, которая в на- шем случае означает в первую очередь то, что они математически корректны? ✓ Как достичь стабильности и надёжности функционирования нашего программного обеспечения? 4 Несмотря на то, что эти два вопроса тесно связаны, фактически они •Р относятся к двум различным областям. Функция может быть мате- матически некорректна, например, если был неправильно реализо- ван базовый алгоритм, и всё же она может надёжно и стабильно воспроизводить эту ошибку и постоянно выдавать один и тот же неправильный результат для заданного входного значения. С дру- гой стороны, функции, возвращающие правильные на вид резуль- таты, могут быть подвержены ошибкам другого рода, например ... таким, как переполнение длины вектора или использование непра- вильно инициализированных переменных, что приводит к неопре- р делённости поведения. Причём эта неопределённость остаётся невыявленной после удачного (или лучше сказать неудачного?) завершения отладки. 4J>. Итак, мы должны иметь в виду оба эти аспекта и утвердить методы 4 < разработки и тестирования, которые позволят нам доверять как кор- ректности, так и надёжности наших программ. Существуют много- численные публикации, в которых обсуждается значение и послед- ’ ствия таких требований для всего процесса разработки программ- ного обеспечения и где углублённо изучается проблема качества программ. Такое почтительное внимание к этой теме нашло выраже- ние в международной тенденции внедрять в производство про- граммного обеспечения стандарт ISO 9000. Теперь больше не гово- рят просто о “тестировании’" или “обеспечении качества”, вместо этого слышны разговоры об “управлении качеством” или о “полном
272 Криптография на Си и C++ в действии управлении качеством”. Отчасти это просто результат эффективного маркетинга, но, тем не менее, эти формулировки должным образом освещают проблему, состоящую в том, чтобы рассматривать про- цесс создания программного обеспечения во всей его многосторон- ней полноте и посредством этого улучшать его. Часто употребляе- мое выражение “проектирование программного обеспечения”, или “программирование” не может скрыть тот факт, что этот процесс, л гид. если уЧесть его отношение к предсказуемости и точности, едва ли может соперничать с классическим инженерным искусством. । Это сравнение должным образом характеризуется следующим анекдотом. Три инженера - механик, электрик и программист - решили вместе прокатиться на автомобиле. Они уселись в машину, но та отказалась заводиться. Механик сразу же заявил: «Проблема с мотором. Засорена форсунка инжектора». «Чепуха, - возразил элек- 4; трик. - Виновата электроника. Определенно отказала система зажи- гания». После чего программист предложил: «А давайте все выле- зем из машины и опять залезем. Может, тогда она заведется». Оставим трех отважных инженеров с их дальнейшими разговорами _ j и приключениями и рассмотрим некоторые опции, которые были реализованы при создании и тестировании пакета FLINT/C. Прежде всего, упомянем те литературные источники, которыми мы пользо- кьд’ вались. Они не утомляют читателя абстрактными рассуждениями и руководящими указаниями, а оказывают конкретную помощь в ре- шении конкретных проблем, не упуская из виду общей картины1. Каждая из этих книг содержит многочисленные ссылки на другую важную литературу по этой теме: ✓ [Dene] - стандартная работа, рассматривающая процесс разработки программного обеспечения во всей полноте. Книга содержит много методологических указаний, основанных на практическом опыте автора, а также много наглядных и полезных примеров. Снова и снова затрагивается тема тестирования в связи с разнообразными этапами программирования и системного интегрирования, при этом основные концептуальные и методологические правила рассматри- ваются совокупно с практической точки зрения, и всё это объеди- нено тщательно спроектированной системой примеров. ✓ [Harb] - содержит полное описание языка программирования С и стандартной библиотеки С, а также даёт много ценных указаний и замечаний по поводу стандарта ISO. Это необходимое справочное пособие, к которому можно обращаться на каждом шагу. ✓ [Hatt] - очень подробно, в деталях, описывает создание в языке С систем программного обеспечения с критической надёжностью. Указанные здесь источники представляют личную, субъективную выборку автора. Существует много других книг и публикаций, которые также можно было бы включить в этот список, однако за недостатком места и времени они были опущены.
ГЛАВА 12, Стратегия тестирования LINT 273 Типовые примеры и источники ошибок демонстрируются с помо- щью конкретных примеров и статистики - а ведь язык С опреде- лённо предоставляет много возможностей для ошибок. Также при- водятся исчерпывающие методологические советы, следуя которым, можно укрепить доверие к продуктам программного обеспечения. ✓ [Lind] - превосходная, занимательно написанная книга, показы- вающая глубокое понимание автором языка программирования С. Кроме того, автор знает, как передать своё понимание читателю. Многие рассматриваемые темы можно было бы снабдить подзаго- ловком «А вы знаете, что...», и очень немногие читатели смогли бы честно, положа руку на сердце, ответить утвердительно. ✓ [Magu] - рассматривает проектирование подсистем и поэтому представляет для нас особенный интерес. Здесь идёт речь об интер- претации интерфейсов и принципах работы с функциями, имею- щими входные параметры. Разъясняются также отличия между рискованным и защитным программированием. Ещё одна сильная сторона этой книги - эффективное использование утверждений (см. стр. 173) в качестве средств тестирования и во избежание неопределённых программных состояний. ✓ [Murp] - содержит описание множества средств тестирования, которые можно, не прилагая больших усилий, применить на прак- тике при тестировании программ и немедленно получить успеш- ные результаты. Помимо всего прочего к этой книге прилагается дискета с библиотеками для реализации утверждений, тестирова- ния обработки объектов динамической памяти, и отчёта о выпол- нении тестов. Эти библиотеки также использовались для тестиро- вания FLINT/C-функций. ✓ [Spul] - предлагает для обозрения методы и средства тестирования программ на языках С и C++ и даёт многочисленные указания по их эффективному применению. Книга содержит широкий обзор ти- пичных для С и C++ ошибок программирования и рассматривает способы их обнаружения и устранения. 12.1. Статический анализ Методологические подходы к тестированию можно разделить на две категории: статическое тестирование и динамическое тести- рование. К первой категории относится проверка кода (текста про- граммы). При этом исходный текст внимательно просматривается и i проверяется построчно на наличие таких проблем, как отклонения , от спецификации (в нашем случае это выбранные алгоритмы), ошибки в рассуждениях, неточности в расположении строк или в t стиле, сомнительные конструкции и ненужные кодовые последова- тельности.
274 Криптография на Си и C++ в действии Для проверки кодов используются аналитические средства, такие как хорошо известная в Unix программа lint, которые в значительной степени автоматизируют эту трудоёмкую задачу. Первоначально одним из главных применений lint было компенсировать существо- вавшие ранее в языке С недочёты при проверке согласования пара- метров, которые передавались функциям в транслируемые по от- дельности модули. Тем временем появились более удобные, чем классический lint, продукты, которые могли обнаруживать огром- ное количество потенциальных проблем в коде программы. Причём синтаксические ошибки, не позволяющие компилятору трансли- ровать код, представляли лишь малую часть этих проблем. Ниже приводятся несколько примеров проблемных областей, которые можно обнаружить путём статического анализа: ✓ синтаксические ошибки, ✓ пропущенные или несогласованные прототипы функций, ✓ несогласования при передаче параметров функциям, ✓ ссылки на несовместимые типы или соединение таких типов, ✓ использование неинициализированных переменных, ✓ непереносимые конструкции, ✓ необычное или неправдоподобное использование отдельных языко- вых конструкций, ✓ недостижимый код. Настоятельным условием строгой проверки типов автоматизиро- ванными средствами является использование прототипов функций. С помощью прототипов ISO-совместимый компилятор языка С может проверять во всех модулях типы передаваемых функциям аргументов и определять несогласования. Многие компиляторы тоже можно настроить на анализ исходного кода, если включены соответствующие уровни предупреждений. Например, компилятор языка C/C++ gcc из проекта GNU Free Software Foundation обладает весьма мощными анализирующими функциями, которые можно активировать с помощью опций -Wall -ansi и -pedantic2 3. При установке FLINT/C-функций, кроме тестов, выполняемых множеством разных компиляторов, для статического тестирования прежде всего применялись продукт PC-lint из Gimpel Software (версия 7.5; см. [Gimp]) и LCLint из MIT (версия 2.4; см [Evan]) • 2 Этот компилятор содержится в различных дистрибутивах Linux, а также его можно приобрести на http://www.leo.org. 3 LCLint можно скачать из Интернета. Домашняя страничка LCLint находится по адресу http://www.sds.lcs.mit.edu/pub/lclint/. По анонимному ftp можно скачать LCLint для Linux И Windows 9x/NT по адресу ftp://sds.lcs.mit.edu/pub/lclint/. I
ГЛАВА 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, в результате чего выполняется соответст- вующий код. С другой стороны, необходимость специальных тес- товых данных при делении с меньшими делителями становится очевидной, только если учитывать, что этот процесс выполняется особой частью функции divj(). Здесь подразумевается именно поД' робность реализации, которую нельзя вывести из алгоритма. На практике обычно всё сводится к смешанному применению ме- тодов чёрного и белого ящиков, которое в работе [Dene] называется соответственно тестирование серого ящика. Однако, никогда нельзя ожидать достижения стопроцентного охвата, как показываю? следующие рассуждения: допустим, что мы генерируем просты6 числа с проверкой по тесту Миллера-Рабина с большим число*1 итераций (скажем, 50) и соответствующей вероятностью ошибки
кАВА 12. Стратегия тестирования LINT 277 ((1/4)“50 ~ 10~30, см. п. 10.5) и затем проверяем эти простые числа ещё одним, детерминированным, тестом на простоту. Поскольку управление передаётся той или иной ветви программы, в зависимо- сти от исхода этой второй проверки, у нас практически нет реаль- ного шанса достичь той ветви, переход к которой следует только после отрицательного результата проверки. Однако вероятность того, что эта вызывающая подозрение ветвь будет выполняться при действительном использовании программы, также нереальна, поэтому, наверное, легче обойтись без этой части теста, чем вно- сить в код семантические изменения ради искусственного создания возможности тестирования. Таким образом, на практике всегда можно встретить ситуации, где требуется отказаться от стопро- центного тестового охвата, чем бы он ни измерялся. Тестирование арифметических функций пакета FLINT/C, который ? г реализован главным образом с математической точки зрения - весьма сложная задача. Как установить, выдают ли правильные ре- зультаты операции сложения, умножения, деления или возведения в степень больших чисел? Карманные калькуляторы, как правило, вычисляют только порядок величины, эквивалентной вычисляемой ' стандартными арифметическими функциями С-компилятора, по- этому значимость этих вычислений ограниченна. Конечно, имеется вариант, при котором можно применить в качест- ве теста другой арифметический программный пакет. Разработаем необходимый интерфейс, преобразуем числовые форматы - и пусть ! функции «соревнуются» друг с другом. Однако против этого под- хода есть два «но»: во-первых, это не развлечение, а во-вторых, r ' почему нужно доверять чьим-то разработкам, о которых известно значительно меньше, чем о собственном продукте? Поэтому по- ищем другие возможности тестирования, и с этой целью применим математические структуры и законы, достаточно мощные, чтобы п; распознать вычислительные ошибки в программе. С обнаружен- ными ошибками затем можно будет разобраться с помощью допол- нительного вывода тестовых результатов и современного символь- uiv ного отладчика. * . , Поэтому мы избирательно применим метод черного ящика и будем надеяться, что к концу этой главы мы составим практичный план проведения динамических тестов, который будет придерживаться того курса проведения тестирования, который применялся к 4 ' FLINT/C-функциям. В ходе этого процесса у нас была цель достичь наибольшего С1-охвата, хотя никакие измерения в этом отношении не производились. Перечень свойств FLINT/C-функций, которые нужно протестиро- вать, не особенно велик, но затрагивает существенные вопросы. В частности, мы должны убедиться в следующем. ✓ Все результаты вычислений корректны над всей областью опреде- ления всех функций.
278 Криптография на Си и C++ в действии ✓ Все входные значения, для которых внутри функции имеются спе- циальные последовательности команд, обрабатываются правильно. ✓ Осуществляется правильное управление переполнением и потерей значимости. То есть все арифметические операции выполняются по модулю Nmax + 1. ✓ Ведущие нули допускаются, не влияя на результат. ✓ Вызовы функции в режиме сумматора с идентичными объектами памяти в качестве переменных, например, такие как addj(nj, nJ, nJ), возвращают правильные результаты. ✓ Все деления на нуль распознаются, и выдаётся соответствующее сообщение об ошибке. Имеется много отдельных тестовых функций, необходимых для ра- боты с этим списком, функций, которые вызывают тестируемые FLINT/C-операции и проверяют их результаты. Эти функции соб- раны в тестовых модулях, и их самих проверяют каждую по от- дельности, прежде чем применить к FLINT/C-функциям. Для проверки тестовых функций используются те же самые критерии и те же самые средства статического анализа, что и для FLINT/C- функций. Более того, выполнение тестовых функции следует просмотреть пошагово по крайней мере на имеющейся в наличии контрольной базе с помощью символьного отладчика для того, чтобы проверить, тестируют ли они то, что нужно. Чтобы опреде- лить, действительно ли тестовые функции должным образом реаги- руют на ошибки, полезно встроить в арифметические функции ошибки, приводящие к неправильным результатам (а после этапа тестирования удалить их, не оставив и следа!). Поскольку мы не можем протестировать каждое значение из области определения CLINT-объектов, нам требуются, кроме фиксированных заданных заранее тестовых значений, случайным образом сгенери- рованные входные значения, которые равномерно распределены по всей области определения [0, Л/тах]. С этой целью используем нашу функцию randj(rj, bitlen), при этом выбираем число двоичных раз- рядов bitlen, с помощью приведения функции usrand64J() по модулю (МАХ2 + 1) случайным образом из интервала [О, МАХ2]. Первыми должны проходить тестирование функции для генерации псевдослу- чайных чисел, которые были рассмотрены в главе 11, где среди прочего мы применяли описанный там критерий %2 для проверки статистических свойств функций usrand64J() и usrandBBSJO- Кр°* ме этого, мы должны убедиться, что функции randJO и randBBSjO правильно генерируют числовой формат CLINT и возвращают числа точно той длины, которая предопределена. Эта проверка необходима и для других функций, выдающих CLINT-значения. Для распозна- вания ошибочных форматов CLINT-аргументов у нас есть функция vcheckj(), которую поэтому следует поместить в начало последова- тельности тестов.
ГЛАВА 12. Стратегия тестирования LINT 279 ОН -к -qu 1 „ \ * >[»-? *'А- Ещё одно условие для большинства тестов - это возможность оп- ределения равенства или неравенства и сравнения размеров целых чисел, представленных CLINT-объектами. Мы должны также про- тестировать функции IdJ(), equj(), mequj() и cmpj(). Это можно осуществить, используя как определённые заранее, так и случайные числа, при этом следует проверять все случаи - равенство, так же как и неравенство - с соответствующими соотношениями размеров. Ввод заранее заданных значений производится, в зависимости от цели, либо посредством функции str2clint_l(), либо в виде типа unsigned посредством функции преобразования u2clintj() или ul2clintj(). Функция str2clintj(), обратная к функции xclint2strj(), используется для генерации выходного результата теста. Поэтому эти функции должны стоять следующими в нашем списке тести- руемых функций. При тестировании строковых функций мы вос- пользуемся их взаимодополняемостью и проверим, получается ли в результате выполнения одной функции после другой исходная строка символов или, при выполнении в другом порядке, выходное значение в CLINT-формате. Далее мы неоднократно будем возвра- щаться к этому правилу. Теперь остается проверить только динамические регистры и их управляющие механизмы, описанные в главе 9, которые вообще хо- телось бы включить в тестовые функции. Использование регистров как динамически распределённой памяти помогает нам тестировать FLINT/C-функции, при этом мы дополнительно реализуем отладоч- ную библиотеку для функций malloc() для распределения памяти. Типовая функция таких пакетов, как общедоступных, так и коммер- ческих (см. [Spul], глава 11), проверяет поддержание границ дина- мически распределённой памяти. Имея доступ к CLINT-регистрам, мы можем следить за нашими FLINT/C-функциями: о каждом вторжении границы в чужую область памяти будет сообщаться. Типовой механизм, предоставляющий такую возможность, перена- правляет обращенные к malloc() вызовы специальной тестовой функции, которая получает запросы на выделение памяти, по оче- реди вызывает malloc() и таким образом выделяет значительно больше памяти, чем требуется на самом деле. Этот блок памяти регистрируется во внутренней структуре данных, а справа и слева от первоначально запрашиваемой области памяти создаётся «барьер» из нескольких бит, которые заполняется избыточным шаблоном, таким как чередующиеся двоичные нули и единицы. Затем возвра- щается указатель на свободную память внутри этого барьера. Теперь обращение к функции free() также направляется сначала к отладчику этой функции. Прежде чем освобождается выделенный блок, выполняется проверка того, остался ли «барьер» неповреж- дённым или шаблон уничтожен путём затирания, и в этом случае генерируется соответствующее сообщение и область памяти вычёр- кивается из регистрационного списка. Только потом в действитель- ности вызывается функция free(). В заключение можно, используя
280 Криптография на Си и C++ в действии внутренний регистрационный список, проверить, какие области памяти не были освобождены. Настройка кода на передачу вызо- вов, обращенных к malloc() и free(), их отладочным вариантам осу. ществляется с помощью макросов, которые, как правило, описаны в файлах #include. Для тестирования FLINT/C-функций применяется пакет ResTrack описанный в [Мигр]. Его использование даёт возможность обнару. жить, при определённых обстоятельствах, трудно уловимые случаи превышения векторных границ CLINT-объектов, которые иначе могли бы остаться необнаруженными во время тестирования. Теперь, когда мы закончили основные приготовления, рассмотрим функции, выполняющие основные вычисления (см. главу 4). add_l(), subj(), mulj(), sqrj(), div_l(), modj(), inc_l(), decj(), shlj(), shr_l(), shiftJO, включая базовые функции add(), sub(), mult(), umul(), sqrQ, смешанные арифметические функции с аргументами типа USHORT uaddj(), usubj(), umulj(), udivj(), umodJO, mod2J(), и, наконец, функции модульной арифметики (см. главы 5 и 6) maddj(), msubj(), mmulj(), msqrj(), и функцию возведения в степень *mexp*J(). Правила вычислений, которыми мы будем пользоваться при тести- ровании этих функций, вытекают из групповых законов для целых чисел, которые уже приводились в главе 5 для кольца классов вычетов 2Л. Здесь мы снова приводим подходящие правила ДяЯ натуральных чисел и можем придумать тест в любом случае, если между выражениями стоит знак равенства (см. таблицу 12.1). Таблииа 12.1. Групповой закон лля целых чисел, используемый при тестировании Сложение Умножение Т ождествен ность а + 0 = а а • 1 =а Коммутативный закон а + b = b + а а . b = b • а Ассоциативный закон (а + Ь) + с = а + (Ь + с) (а • Ь) . с = а • (Ь • d
ГЛАВА 12. Стратегия тестирования LINT 281 Сложение и умножение можно проверять относительно друг друга, используя определение ПК"’ - • Э к := я, /=1 по крайней мере, для малых значений к. Следующие соотношения, которые поддаются тестированию, - это дистрибутивный закон и иг- . J Г Л ‘j i первая формула бинома Ньютона. Закон дистрибутивности: а • (Ь + с) = а • b + а • с Формула бинома Ньютона: (а + b)2 = а2 + 2аЬ + Ь2 Законы сокращенного сложения и умножения предоставляют сле- дующие возможности для проверки сложения и вычитания, так же как умножения и деления: а +b = c=>c-a = b\\c~b = a И а • b = с => с/а = b и с!Ь = а. Деление с остатком можно проверить умножением и сложением, используя функцию деления, чтобы вычислить для делимого а и делителя Ь, сначала частное q и остаток г. Затем в игру вводятся умножение и сложение, чтобы проверить, выполняется ли равенство а = Ь- q + г. Для проверки модульного возведения в степень с помощью умно- жения для малых к мы прибегаем к помощи определения: /=1 Отсюда можно перейти к правилам возведения в степень (см. главу 1) ars = (аУ ar+s = а • а\ которые являются основой для проверки возведения в степень умножением и сложением. Кроме этих и других тестов, основанных на правилах арифметиче- ских вычислений, мы используем специальные тестовые подпро- граммы, которые проверяют оставшиеся функции из вышеприве- дённого списка, и, в частности, поведение этих функций на грани- цах областей определения CLINT-объектов или в других ситуациях, критических для отдельных функций. Некоторые из этих тестов находятся в тестовом комплекте пакета FLINT/C, входящем в
282 Криптография на Си и C++ в действии сопроводительный CD-ROM. Этот тестовый пакет содержит модули, перечисленные в таблице 12.2. Таблииа 12.2. Имя модуля Содержание теста Тестовые testrand.с Линейные сравнения, генератор псевдослучайных функции пакета FLINT/C чисел testbbs.c Генератор псевдослучайных чисел Блюм-Блюма-Шуба testreg.с Управление регистрами testbas.c Базовые функции cpy_l(), ld_l(), equ_l(), mequ_l(), cmpJO, u2clint_l(), ul2clint_J(), str2clint_l(), xclint2str_l() testadd.с Сложение, включая inc_J() testsub.с Вычитание, включая dec_l() testmul.c Умножение testkar.c Умножение методом Карацубы testsqr.c Возведение в квадрат testdiv.c Деление с остатком testmadd.c Модульное сложение testmsub.c Модульное вычитание testmmul.c Модульное умножение testmsqr.c Модульное возведение в квадрат testmexp.c Модульное возведение в степень testset.c Функции доступа к битам testshft.c Операции сдвига testboo I.c Булевы операции testiroo.c Извлечение целого квадратного корня эд testggt.c Вычисление наибольшего общего делителя и наименьшего обшего кратного Мы вернемся к нашим теоретико-числовым функциям в конце вто- рой части этой книги, где они приведены в качестве упражнении для заинтересованного читателя (см. главу 17).
Часть II Класс LINT: арифметика на C++ Анатомические находки, составляющие пред- мет исследования науки, широко распростра- нены в качестве украшений при создании объ- ектов в различных географических зонах и антропо-этнических группах. Добытый чело- веком фрагмент, обычно кость, становится функциональной деталью конструкции объек- тов. Кость, как минимум, частично утрачивает свою анатомическую индивидуальность, в том смысле, что ее обрабатывают, обращаясь с ней так, что она становится неотъемлемой частью объекта, тем самым приобретая символическое значение, выходящее далеко за пределы телес- ной сущности. Надпись на этикетке экспоната национального музея Антропологии и Этнологии, Флоренция, Италия.
ГЛАВА 13. Пусть C++ облегчит Вашу жизнь Детали разменяли нашу жизнь на мелочи... Упрощайте, упрощайте. А. Д. Торо, Вальден. Язык программирования C++, разрабатывавшийся с 1979 года Бьярном Страуструпом1 в Bell Laboratories, является расширением языка С, и его роль становится преобладающей по отношению к другим языкам в области создания программных продуктов. Язык C++ поддерживает принципы объектно-ориентированного про- граммирования, основой которого являются программы, а точнее х сказать, процессы, включающие в себя множества объектов, кото- рые взаимодействуют исключительно через их интерфейсы. То есть они обмениваются информацией или принимают определенные внешние команды и обрабатывают их как задачу. В этом интерфейсе методы, с помощью которых выполняется задача, являются подза- дачей, “определенной на” автономии единственного объекта. Структуры данных и функции, которые представляют внутреннее состояние объекта и эффект переходов между состояниями, явля- 11 ются частным делом объекта и не должны быть обнаружены со стороны. Данный принцип, известный как намеренное скрытие 5 информации от пользователя, помогает разработчикам программ- ного обеспечения сосредоточиться над задачами, в которых объект ' выполняется внутри структуры программы, что позволяет не вда- ij ' ваться в детали реализации. (Говоря другими словами, мы заостряем внимание на том, “что”, а не “как”). ‘„ю. • п / Структурные единицы, которые имеют дело с «частными делами» m объекта и содержат полную информацию об организации структур данных и функций, называются классами. Наряду с этим создаются я внешние интерфейсы объекта, которые и определяют набор пове- vr дений, который объект может выполнять. Так как все объекты 1хг; класса имеют одинаковый самый структурный дизайн, они тоже Интернет-страница Бьярна Страуструпа (hhtp://www.research.att.comfbs/), возможно, поможет °тветить на вопрос «Как Вы произносите “Бьярн Страуструп”?»: “Это может быть затрудни- тельным для людей, которые не являются скандинавами. Лучший совет, который я до сих пор слышал, это ‘начать выговаривать имя и фамилию несколько раз на норвежском языке, затем Набить горло картошкой и повторить это снова’:-). Оба моих имени произносятся по слогам: Pjar-ne Strou-strup. Как В, так и J в моем имени не являются ударными, a NE - достаточно Слабые, поэтому Be-ar-neh или Ву-ar-ne наталкивает на правильный путь. Первое U в моей фами- лии на самом деле должно было быть V, первый конечный слог произносится глубоким гортан- HbiM голосом: Strov-strup. Второе U слегка похоже на 00 в OOP, однако оно остается коротким; в°зможно Strov-stroop подаст какую-нибудь идею. ” (Как видно, устоявшаяся русская транс- крипция также имеет мало общего с оригиналом, имя должно звучать как что-то похожее на ^ьярне Стравструп» — Прим, перев.)
286 Криптография на Си и C++ в действии обладают одним и тем же интерфейсом. Но как только объекты бы- ли созданы (компьютерные разработчики говорят, что класс тира- жирует (instantiate) объекты), они существуют независимо. Их внутреннее состояние меняется независимо друг от друга, кроме того, они запускают различные задачи в соответствии с их ролями в программе. Объектно-ориентированное программирование распространяет ис- пользование классов в качестве стандартных блоков больших структур, которые также могут являться классами или группами классов, в готовые программы, так же, как и дома или автомобили делаются из сборных модулей. В идеальном варианте, программы могут быть собраны вместе из библиотек заранее созданных классов без необходимости создания значимой части нового кода (во всяком случае, не в такой степени, как для традиционной разра- ботки программ). В результате этого проще разрабатывать про- грамму применительно к текущей ситуации, прямо моделировать текущие процессы, таким образом проходя последовательные уточнения, пока результатом не будет набор объектов отдельных классов и их взаимосвязей, причем все еще можно распознать модель реальности, которая лежит в их основе. Такой порядок выполнения действий достаточно хорошо известен нам из многих жизненных аспектов, то есть мы не работаем напря- мую с необработанными материалами, если мы хотим что-либо создать скорее мы станем применять готовые модули, о конструк- ции которых или внутренней разработке в деталях мы ничего не знаем. Просто в этих знаниях нет необходимости. Опираясь на имеющийся опыт, мы получаем возможность создания все более и более сложных структур. В процессе создания программ предыду- щий опыт разработок раньше не находил применения, разработчики программных продуктов постоянно возвращались к тому, что соз- давали ранее. Программы создавались с помощью элементарных операций языка программирования (такой конструктивный процесс обычно называют кодированием). Применение библиотек этапа исполнения, таких как стандартная библиотека С, не улучшает существенно эту ситуацию, так как функции, содержащиеся в подобных библиотеках, являются слишком примитивными, чтобы обеспечить связь с более сложными приложениями. Любой программист знает, что структуры данных и функции, кото- рые являются подходящим решением для некоторых проблем, лишь изредка можно применить в похожих, но тем не менее разных задачах без модификации. И как результат - практическое отсут ствие выгоды от оттестированных и доверенных компонентов, любое их изменение содержит риск появления новых ошибок - к в проектировании, так и в программировании. (Это напомина предупреждение в инструкции по эксплуатации любого товару “Любое изменение, вносимое лицом, не являющимся авторизовав ним специалистом, отменяет гарантию”).
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 287 С целью повторного использования программ в форме готовых компонент, среди огромного количества других концепций был разработан принцип наследования. Это дает возможность модифи- цировать классы, для того чтобы удовлетворять новым требованиям, фактически не изменяя их. Вместо этого необходимые изменения будут внесены на уровне расширений. Объекты, которые появились таким образом, помимо новых свойств, приобретают все свойства старых объектов. Можно сказать, что они наследуют эти свойства. Принцип скрытия информации остается незыблемым. Вероятность ошибки значительно уменьши- лась, а производительность возросла. Все это выглядит так, словно сбываются мечты. Как объектно-ориентированный язык программирования, C++ обладает всеми необходимыми механизмами для поддержки этих принципов абстракции2. Тем не менее, эти механизмы представ- ляют собой лишь возможность, а не гарантию того, что они будут использоваться так, как принято в объектно-ориентированном программировании. С другой стороны, переход от традиционной к объектно-ориентированной разработке программного обеспечения ' требует значительной интеллектуальной перестройки. Особенно явно это отражается в двух отношениях: с одной стороны, разра- ботчик, который к настоящему времени достиг больших успехов, вынужденно посвяшает значительно больше внимания фазам моде- лирования и проектирования, нежели тому, что обычно требовалось в традиционных методах разработки программ. С другой стороны, при разработке и тестировании новых классов особое внимание нужно обращать на то, чтобы компоновочные блоки были безоши- 3 бочны, так как они будут использоваться в множестве программ, которые будут разрабатываться в дальнейшем. Также скрытие ип информации может означать скрытие ошибок, Исказится цель идеи f объектно-ориентированного программирования, если пользователю Ир.'*класса придется знакомиться с внутренней его организацией для |»г того, чтобы найти ошибку. Результатом этого являются ошибки, Ьн содержащиеся в реализации класса, которые наследуются вместе с |Вв 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] здесь перечислено несколько И3 наиболее важных названий. За основу попыток стандартизации ISO был взят [ElSt], который ст< стандартом. ।
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 289 >7’ ’•' Объекты генерируются как экземпляры класса при помощи конст- рукторов, которые осуществляют распределение памяти, инициа- лизацию данных и другие задачи управления перед тем как объект будет готов к выполнению работы. Нам потребуется несколько по- добных конструкторов для того, чтобы сгенерировать LINT-объекты из различного контекста. Наряду с конструкторами существуют и 5 : деструкторы, занимающиеся удалением объектов, которые больше не нужны, и ресурсов, которые были им выделены. Вот элементы языка C++, которые мы обычно используем в разра- ботке наших классов: ✓ перегрузка операций и функций; ✓ усовершенствованные возможности, по сравнению с С, для ввода и вывода. В следующих частях рассматривается применение этих двух прин- ципов при разработке нашего класса LINT. Для того чтобы у чита- теля появилось представление о том, что из себя представляет класс LINT, мы покажем небольшую часть его объявления: class LINT г i public: LINT (void); // Конструктор -LINT (); //Деструктор ' HI- К s - J » OX", ’ * • const LINT& operator (const LINT&); const LINT& operator+= (const LINT&); const LINT& operators (const LINT&); 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
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: clint *nj; int maxlen; int init; int status; }; Можно выделить в классе LINT типовое подразделение на два блока: первый, открытый блок с конструктором, деструктором, арифмети- ческими операторами и функциями-членами и друзьями данного класса. Небольшой блок закрытых элементов данных присоединен к открытому интерфейсу и идентифицирован меткой private. Такое деление используется для большей ясности и хорошим тоном счи- тается расположить открытый интерфейс перед закрытым блоком, а метки public и private использовать только один раз внутри каждого объявления класса. Приведенный здесь список операторов, который фигурирует в раз- деле объявления класса, очевидно, еще не совсем завершен. В нем не хватает некоторых арифметических функций, которые не могут быть представлены в качестве операторов, так же как и большинство теоретико-числовых функций, которые нам уже известны как функ- ции языка С. Более того, объявленные конструкторы представлены так же мало, как и функции ввода и вывода объектов LINT. В следующем списке параметров операторов и функций появляется ссылочный оператор &, в результате применения которого объекты класса LINT передаются не по значению, а по указателю на объект. Аналогично происходит и при возврате. Такое использование & недопустимо в С. Однако при более доскональном рассмотрении ii
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 291 ’ nWra<: можно заметить, что только лишь определенные функции-члены возвращают указатель на объект LINT, в то время как многие другие возвращают в качестве результата само значение. Основным пра- вилом, которое определяет, какой из двух методов используется, является то, что функции, изменяющие один или более аргументов, которые им передаются, могут вернуть результат в качестве ссыл- ки, тогда как другие функции, не изменяющие входные параметры, возвращают результат как значение. По мере изучения мы увидим, в каких LINT-функциях используется какой способ. Классы в языке C++ являются расширением сложного типа данных «структура» в Си, и доступ к элементу х класса производится синтаксически точно так же, как и к элементу структуры, то есть, например, А.х, где А - указывает на объект, а х - на элемент класса. Также следует отметить, что в списке параметров функции-члена аргумент имеет неполное имя в отличие от точно так же названной функции-друга, что показано в следующем примере: friend LINT gcd (const LINT&, const LINT&); в сравнении с : LINT LINT::gcd (const LINT&); Поскольку функция gcd() в качестве функции - члена класса LINT принадлежит объекту А типа LINT, вызов gcd() должен происхо- дить в форме A.gcd(b) без появления А в списке параметров. Однако дружественная функция gcd() не принадлежит ни одному объекту и таким образом не содержит неявных аргументов. Мы наполним указанную выше упрощенную структуру нашего класса LINT в следующих главах и выявим множество ее особенно- стей, для того чтобы со временем у нас была окончательная реали- зация класса LINT. Те, кому интересно узнать в общем о C++, могут обратиться к следующим ссылкам: [Deit],[EISt],[Lipp], а также [Meyl] и [Меу2]. 13.1. Частное дело: представление чисел в классе LINT Если мои идеи не похожи на их мнение, Оставлю лучше их при своей точке зрения. А. Е. Хаусман, Последние поэмы IX Представление длинных чисел, которые были выбраны для нашего класса, является расширением их представления на С, описанного в I части. Оттуда мы возьмем расположение цифр натурального числа как вектор значений типа clint, в котором наиболее старшие 10*
292 Криптография на Си и C++ в действии разряды располагаются по старшему индексу (см. главу 2). Память, которая требуется для этого, автоматически выделяется в момент генерации объекта. Этот процесс осуществляется конструкторами, которые вызываются как явно - в программе, так и неявно - при компиляции с помощью new(). Поэтому в объявлении класса нам потребуется переменная типа clint *nj, с которой в рамках одного конструктора и ассоциируется указатель на размещаемую там память. В качестве второго элемента нашего числового представле- ния мы определяем переменную maxlen, которая хранит количество памяти, выделенной конструктором отдельному объекту. Перемен- ная maxlen определяет максимальное число clint-разрядов, которые может иметь объект. Более того, мы хотим установить, был ли LINT-объект инициализирован, то есть было ли ему присвоено’ какое-нибудь числовое значение перед тем как он был использован . Bq ж справа от знака равенства в числовом выражении. Поэтому мы вво- дим переменную init целого типа, которая изначально имеет значе- ние 0 и устанавливается в 1, когда впервые объекту присваивается! числовое значение. Мы реализуем наши функции и операторы класса LINT так, чтобы сообщение об ошибке выдавалось в случае, если не определены значения объекта LINT, а, как следствие, и значение выражения. Переменная status, строго говоря, не является элементом нашего представления в числовом виде. Ее применяют для индикации ситуации переполнения или потери значимости (см. стр. 32), если оно происходит в результате выполнения операций над объектами LINT. Типы и механизмы сообщений об ошибках и обработка оши- бок подробно описаны в главе 15. Таким образом, класс LINT определяет совокупность следующих элементов для представления целых чисел и хранения состояний объектов: clint* 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) { nJ = new CLINT; if (NULL == nJ) { panic (EJJNT.NHP, "конструктор 1", 0,_LINE_); } maxlen = CLINTMAXDIGIT; init = O; status = EJJNTJDK; } Если заново порожденный объект инициализируется числовым значением, то подходящий конструктор должен способствовать генерации объекта 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++ в действии образовывать строку символов в объект типа CLINT, из которого на втором шаге будет создан объект 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 = str2clintj (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 = str2clintj (nJ, (char*)str, 10); } } switch (error) // оценка кода ошибки { case 0: maxlen = CLINTMAXDIGIT; init = 1; status = E_LINT_OK; break; case E_CLINT_BOR:
295 ГЛАВА 13. Пусть C++ облегчит вашу жизнь panic (E_LINT_BOR, ” конструктор 4”, 1,_LINE_); break; case E_CLINT_OFL: panic (E_LINT_OFL," конструктор 4", 1,_LINE__); break; case E_CLINT_NPT: panic (E_LINT_NPT," конструктор 4", 1,_LINE__); break; default: panic (E_LINTJERR, ” конструктор 4”, error,_LINE ); } } Таблица 13.1. Конструкторы Конструктор Семантика: создание объекта LINT LINT (void); LINT (const char* const, const unsigned char); LINT (const UCHAR* const, const int) LINT (const char* const); LINT (const LINT&); Без задания начального значения (конструктор по умолчанию) Из строки символов с основанием числового представления, заданной во втором аргументе Из вектора байтов с длиной, заданной во втором аргументе Из строки символов, возможно с префиксом ОХ для шестнадиатиричных чисел или ОБ для бинарных Для других объектов LINT (конструктор копирования) LINT тчг LINT (const int); Из значения типа char, short или int НГ LINT (const long int); Из значения типа long int k; I LINT (const UCHAR); Из значения типа UCHAR LINT (const USHORT); Из значения типа USHORT LINT (const unsigned int); Из значения типа unsigned int LINT (const ULONG); LINT (const CLINT); Из значения типа ULONG Из объекта CLINT Конструкторы позволяют проводить инициализацию объектов LINT другими такими объектами, так же как и стандартными ти- пами, константами и символьными строками, что и показано на следующем примере:
Криптография на Си и C++ в действии LINT а; LINT one (1); int i = 2147483647; LINTb (i); LINT c (one); LINT d ("0x123456789abcdef0"); Функции-конструкторы вызываются явно для генерации объектов типа LINT из заданных аргументов. Конструктор LINT, который, например, преобразует значения типа unsigned long в объекты LINT, реализован в следующей функции: LINT::LINT (const ULONG ul) { nJ = new CLINT; if (NULL == nJ) { panic (EJJNTJMHP, ’’Конструктор 11”, 0, _LINE_); } ul2clintj (nJ, ul); maxlen = CLINTMAXDIGIT; init = 1; status = E_LINT_OK; } А теперь нам необходимо получить функцию-деструктор, которая соответствует конструкторам класса LINT и которая дает возмож- ность освободить объекты, в частности, связанную с ними память. Вообще говоря, компилятор охотно создает деструктор по умолча- нию, которым можно пользоваться, однако он освобождает только память, которую занимают элементы объекта LINT. Дополнитель- ная память, которая выделяется конструктами, не освобождается, в результате чего идет утечка памяти. Следующий далее короткий деструктор выполняет важную задачу освобождения памяти, зани- маемой объектами LINT. ~LINT() { delete [] nJ;
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 297 13.3. Перегрузка операторов. Перегрузка операторов представляет собой достаточно мощный механизм, позволяющий определить функции с одинаковыми име- нами, но с различным списком параметров, то есть которые могут выполнить отличающиеся операции. Компилятор использует спе- циальный список параметров для того, чтобы определить, какая функция имеется в виду. Чтобы сделать это, C++ использует стро- гий контроль типов, который позволяет избежать двусмысленности или противоречивости. Перегрузка фукнций-операторов позволяет применять “обычный” способ записи выражения суммы с = а + b с объектами а, b и с клас- ( са LINT вместо того, чтобы вызвать функцию, например, addjfaj, bj, сJ). Это позволяет осуществить органичную стыковку класса с ? языком программирования, а также значительно повышает чита- бельность программ. Для данного примера необходимо перегрузить оператор “+” и присваивание “=”. Существует всего несколько операторов в C++, которые нельзя пере- гружать. Даже оператор “[ ]” который применяется для получения доступа к векторам, может быть перегруженным, например, функци- ей, одновременно проверяющей, не превышают ли запрошенный ин- декс вектора его границ. Однако не нужно забывать, что перегрузка операторов открывает путь к возможным неприятностям. В частно- сти, не могут быть изменены операторы над стандартными типами данных; так же не может быть изменен заранее определенный при- оритет операторов (см. [Strl], Раздел 6.2) или “созданы” новые опе- раторы. Но для отдельных классов вполне возможным является оп- --------- ределение функции-оператора, не имеющего ничего общего с тем и-----оператором, с которым он обычно ассоциировался. Для того, чтобы в программах было проще разобраться, необходимо следовать следую- щему совету - строже придерживаться смысла стандартных операто- ров в C++ при перегрузке, чтобы избежать бесполезной путаницы. Следует отметить, что в структуре класса LINT, отмеченной выше, некоторые операторы выполнялись как функции-друзья, а другие как функции-члены. Причиной этому послужило то, что мы хотели - бы использовать, например, “+” или “*” в двух качествах: когда они могут не только обрабатывать два эквивалентных объекта LINT, но и, как альтернатива, принять один объект LINT и один из встроен- ных целочисленных типов языка C++, более того, принять аргументы в любом порядке, поскольку сложение является коммутативным. Для этой цели нам потребуются ранее описанные конструкторы, которые создают объекты LINT из целых типов. Комбинированные выражения, такие как: LINT а, Ь, с; int number;
298 Криптография на Си и C++ в действии И Инициализация а, b и number и какие-то вычисления /I... с = number * (а + b / 2) также становятся возможными. В обязанности компилятора входит автоматический вызов подходящих функций конструктора. Он также следит за тем, чтобы преобразование целого числа number и константы 2 в объекты LINT происходило в момент выполнения программы, перед тем как будут вызваны операторы + и *. Таким образом мы получим максимально возможную гибкость в приме- нении операторов с .тем лишь ограничением, что выражения, со- держащие объекты типа LINT, сами являются типом LINT и, соот- ветственно, могут быть присвоены только объектам типа LINT. Перед тем как мы углубимся в детали каждого отдельного операто- ра, нужно получить общее представление об операторах, опреде- ленных классом LINT, которые читатель может найти в Таблицах 13.2-13.5. Таблииа 13.2, 4- Сложение Арифметические операторы ++ Инкремент (префиксный и постфиксный) класса LINT - Вычитание - Декремент (префиксный и постфиксный) * Умножение / Деление (частное) fp. % Остаток Таблииа 13.3. & Побитовое И Побитовые операторы I Побитовое ИЛИ класса LINT Л Побитовое исключающее ИЛИ (XOR) « Сдвиг влево j » Сдвиг вправо i Таблииа 13.4. == Равенство Логические операторы != Неравенство класса LINT </<= Меньше, меньше или равно * >,>= Больше, больше или равно
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 299 Таблииа 13,5. Операторы присваивания класса LINT о < ж = Простое присваивание += Присваивание после сложения -= Присваивание после вычитания *_ Присваивание после умножения /= Присваивание после деления %= Присваивание после взятия остатка &= Присваивание после побитового И l= Присваивание после побитового ИЛИ A— Присваивание после побитового исключаюшего ИЛИ «= Присваивание после левого сдвига »= Присваивание после правого сдвига •/>пг '! к 'ШЖШЙМ Сейчас мы хотим обсудить реализацию функций операторов которые могут служить примерами реализации операторов LINT. Сначала с помощью оператора можно увидеть, как выполняется умножение объектов LINT с помощью функции С mulj(). Оператор реализован как функция-друг, в которую оба сомножителя переда- ются по ссылке. Так как функции-операторы не меняют своих аргументов, ссылки объявляются как const (константы): const LINT operator* (const LINT& Im, const LINT& In) { LINT prd; int error; Сначала нужно убедиться, инициализированы ли аргументы 1m и In, переданные по ссылке. Если это не выполняется для обоих ар- гументов, то включается обработка ошибок и вызывается функ- ция-член panicO, которая объявлена как статическая (см. Главу 15) if (llm.init) LINT::panic (E_LINT_VAL, 1, _LINE_); if (lln.init) LINT::panic (E_LINT_VAL, "*", 2, _LINE_); Вызывается С-функния mul_l(), в которую передаются в качестве аргументов: векторы Im.nJ, In.nJ - как множители, a prd.nj - как результат, куда будет помешено произведение. error = mulj (Im.nJ, In.nJ, prd.nj);
Криптография на Си и C++ в действии При опенке кода ошибки, который хранится в переменной error, возможны 3 случая. Если error == 0, то все в порядке и объект prd может быть отмечен как инициализированный. Это делается присваиванием переменной prd.init 1 .Переменная статуса prf.status уже была установлена конструктором в значение E_LINT_OK. Если произошло переполнение в функции mul_l(), то переменная error содержит значение E_CLINT_OFL. Поскольку в этом случае вектор prd.n_l содержит правильное по формату число 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_); } Если ошибка не может быть исправлена функцией panicO, то и возврат в эту точку не произойдет. Механизм распознавания ошибок здесь приведёт к завершению, которое, в принципе, лучше, чем продолжение работы программы в неопределенном состоянии. И как заключающий шаг - поэлементный возврат произведения prd. return prd; } Поскольку объект prd существует лишь внутри контекста функции, компилятор обеспечивает автоматическое создание временного объекта, который представляет значение переменной prd вне функ- ции. Данный временный объект создается с помощью конструктора
ГЛАВА 13. Пусть C++ облегчит вашу жизнь 301 • лЯЬ ! он. -Л'Ш копирования LINT (const LINT&) (см. стр. 295) и существует до тех пор, пока выражение, в рамках которого использовался оператор, не будет обработано, то есть по достижении закрывающей точки с запятой. Так как значения функции объявлены как const, компиля- тор не воспримет такие бессмысленные конструкции как (а * Ь) = с. Основной целью здесь является работа с LINT-объектами теми же способами, как и со встроенными целых типов. Мы можем расширить возможности функции-оператора, используя следующие приемы: если сомножители одинаковы, то умножение заменяется возведением в квадрат, и преимущество в скорости, связанное с этой заменой, может быть применено автоматически (см. п. 4.2.2). Однако придется затратить некоторые усилия на по- элементное сравнение аргументов для того, чтобы определить их равенство, что обходится нам дорого, и следует довольствоваться компромиссом: возведение в квадрат следует выполнять только в случае, если оба сомножителя ссылаются на один и тот же объект. Таким образом, мы проверим, являются ли In и Im указателями на один и тот же объект, и в этом случае вместо умножения выпол- ним возведение в квадрат. Ниже приведен соответствующий текст программы: if (&lm == &ln) a»: error = sqrj (Im.nJ, prd.nj); else error = mulj (Im.nJ, In.nJ, prd.nj); Этот взгляд назад на функции, реализованные в языке С в части I, представляет собой модель для всех оставшихся функций класса LINT, который сформирован как оболочка вокруг ядра функций С и защищает его от пользователей класса. Прежде чем мы обратимся к более сложному оператору присваива- ния лучше, по-видимому, более подробно рассмотреть про- стой оператор присваивания “=”. В части I мы уже определили, что присваивание объектов требует особого внимания (см. главу 8). Следовательно, как и в реализации на С, нужно четко обращать внимание на то, чтобы при присваивании одного объекта класса LINT другому присваивалось содержимое, а не адрес. Точно так же нам нужно для нашего класса LINT определить специальный вари- ант оператора присваивания который выполняет не только простое копирование элементов класса: по тем же причинам, кото- рые были описаны в главе 8, нам следует обратить внимание на то,
Криптография на Си и C++ в действии что копируется не адрес числового вектора nJ, а разряды, на кото- рые он ссылается. Как только стало понятным основное требование к порядку дейст- вий, дальнейшая реализация не будет очень сложной. Оператор “=” реализован как функция-член и возвращает как результат ссылку на неявный левый элемент. Вне всяких сомнений, мы будем ис- пользовать внутри функцию С cpyj() для того, чтобы перенести разряды из одного объекта в другой. Для того чтобы выполнилось присваивание а = Ь, компилятор вызывает функцию-оператор “=” в контексте а, которая берет на себя роль неявного аргумента, не указанного в списке параметров функции-оператора. В рамках функции-члена ссылка на элементы неявного аргумента может быть выполнена просто по имени, без учета контекста. Более того, ссылку на неявный объект можно сделать с помощью специального указателя this, как показано в приведенном ниже примере реализа- ции оператора const LINT& LINT::operator= (const LINT& In) { if (lln.init) panic (E_LINT_VAL, 2, _LINE_); if (maxlen < DIGITSJ_ (In.nJ)) panic (E_LINT_OFL, и=н, 1, _LINE—); Сначала проверим, являются ли ссылки на правый и левый аргу- менты одинаковыми, так как в этом случае нет необходимости копировать. В противном случае, разряды числового представле- ния In копируются в неявный левый аргумент *this, так же как переменные init и status, а возвращается ссылка на неявный эле- мент в виде *this. if (&ln != this) { cpyj (nJ, In.nJ); init = 1; j status = In.status; J } 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 (E_LINT_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 = EJJNTJDK;
Криптография на Си и C++ в действии break; case E_CLINT_OFL: status = EJJNTJDFL; break; default: panic (EJJNT.ERR, "*=n, error, _LINE_); } return *this; } В качестве нашего последнего примера оператора LINT мы опишем функцию “= =”, которая проверяет на равенство два объекта LINT: в результате возвращается значение 1, если они равны, и 0 - в про- тивном случае. Оператор = = также иллюстрирует реализацию дру- гих логических операторов. const int operator== (const LINT& Im, const LINT& In) { j| if (!Im.init) LINT::panic (E_LINT_VAL, "==", 1, _LINE_); if (lln.init) LINT::panic (E_LINT_VAL, "==”, 2, _LINE_); if (&ln == &lm) I return 1; И else И return equj (Im.nJ, In.nJ); И
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса Пожалуйста, примите мою отставку. Я не хочу принадлежать какому-нибудь клубу, который I . / г. примет меня в качестве члена клуба. Гручо Маркс f Каждый раз, когда я пишу портрет, я теряю друга. Джон Сингер Сарджент В добавление к функциям-конструкторам и операторам, о которых говорилось ранее, существуют и дополнительные функции LINT, которые делают функции языка С, рассмотренные в части I, дос- и тупными объектам LINT. На повестке грядущего обсуждения ле- t жит следующее: мы сделаем приблизительное разделение функций .. на “арифметические” и ’’теоретико-числовые” категории. Реализа- цию функций мы обсудим вместе с примерами; в остальных случаях мы ограничимся таблицей, которая требуется для их применения по назначению. Также в следующих разделах мы дадим трактование в более широкой форме функциям форматированного вывода объектов ’**'* 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); Функции mexp() построены так, что вызываемые ими С-функции являются оптимизированными в зависимости от типа операндов (а именно, mexpkJO, mexpkmJQ, umexpJO или umexpmJO), а в со- ответствующих арифметических дружественных функциях мы бу- дем обычно иметь дело с функциями возведения в степень wmexpJO и wmexpmJO с основанием типа USHORT. Функция: Модульное возведение в степень с автоматическим применением возведения в степень Монтгомери, если модуль оказался нечетным. Синтаксис: const LINT& LINT::mexp (const LINT& е, const LINT& m); Вход: неявный аргумент (основание) е (экспонента) m (модуль) Возврат: указатель на остаток 1 Пример: a.mexp (е, т); const LINT& LINT::mexp (const LINT& e, const LINT& m) { int error; if (!init) panic (E_LINT_VAL, "mexp", 0,_LINE_);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 3Q7 if (le.init) panic (E_LINT_VAL, "mexp", 1, _LINE_); if (Im.init) panic (EJJNT_VAL, "mexp", 2, _LINE_); err = mexpj (nJ, 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; } Функция: Модульное возведение в степень Синтаксис: const LINT& LINT::mexp (const USHORT e, const LINT& m); Пример: a.mexp (e, m); const LINT& LINT::mexp (const USHORT e, const LINT& m) г 1 int err; if (linit) 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& т) 1 return *this; } А теперь мы рассмотрим набор дополнительных арифметических и теоретико-числовых функций-членов. Функция: сложение Синтаксис: 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); выполняет операцию а Функция: возведение в квадрат Синтаксис: const LINT& LINT::sqr (void); Вход: неявный аргумент (множитель) Возврат: указатель на неявный аргумент, который содержит квадрат Пример: a.sqr (s); выполняет операцию а *= а; Функция: деление с остатком Синтаксис: const LINT& LI NT: :divr (const LINT& d, LINT& r); Вход: неявный аргумент (делимое) d (делитель) Выход: г (остаток от деления по модулю d) Возврат: указатель на неявный аргумент, который содержит частное Пример: a.divr (d, г); выполняет операцию а /= d; г = а % d;
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() дается свое собственное имя. Функция: проверка на равенство по модулю m Синтаксис: const int ? LINT::mequ (const LINT& b, const LINT& m); Вход: неявный аргумент a второй аргумент b модуль m Возврат: 1, если a = b mod m, в противном случае 0 Пример: if (a.mequ (b, m)) //...
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 311 функция: модульное сложение Синтаксис: const LINT& LINT::madd (const LINT& s, const LINT& m); Вход: неявный аргумент (слагаемое) s (слагаемое) m (модуль) Возврат: указатель на неявный аргумент, который содержит сумму по модулю m Пример: a.madd (s, m); Функция: модульное вычитание Синтаксис: const LINT& LINTr.msub (const LINT& s, const LINT& m); Вход: неявный аргумент (уменьшаемое) s (вычитаемое) m (модуль) Возврат: указатель на неявный аргумент, который содержит разность по модулю m Пример: a.msub (s, m); Функция: модульное умножение Синтаксис: const LINT& LINTr.mmul (const LINT& s, const LINT& m); Вход: неявный аргумент (множитель) s (множитель) m (модуль) Возврат: указатель на неявный аргумент, который содержит произведение по модулю m Пример: a.mmul (s, m);
312 Криптография на Си и C++ в действии Функция: модульное возведение в квадрат Синтаксис: const LINT& LINT::msqr (const LINT& m); Вход: неявный аргумент (множитель) г m (модуль) ? Возврат: указатель на неявный аргумент, который содержит квадрат по модулю m Пример: a.msqr (m); Функция: модульное возведение в степень с показателем вида 2е Синтаксис: const LINT& LINT::mexp2 (const USHORT e, const LINT& m); Вход: неявный аргумент (основание) e (степень 2) m (модуль) Возврат: указатель на неявный аргумент, который содержит степень по модулю m Пример: a.mexp2 (е, m); Функция: модульное возведение в степень (2к-арный метод, с преобразованием Монтгомери) Синтаксис: const LINT& LINT::mexpkm (const LINT& е, const LINT& m); Вход: неявный аргумент (основание) е (показатель) m (нечетный модуль) Возврат: указатель на неявный аргумент, который содержит степень по модулю m Пример: a.mexpkm (е, m);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 313 Функция: модульное возведение в степень (25-арный метод, с преобразованием Монтгомери) Синтаксис: const LINT& LINT::mexp5m (const LINT& е, const LINT& m); Вход: и неявный аргумент (основание) е (показатель) • m (нечетный модуль) Возврат: указатель на неявный аргумент, который содержит степень по модулю m Пример: a.mexp5m (е, 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); Вход: неявный аргумент a pos - позиция того бита, который будет установлен (начиная с 0) Возврат: указатель на а, с установленным битом в позиции pos Пример: a.setbit (512); Функция: проверка двоичного разряда объекта LINT Синтаксис: const int LINT::testbit (const unsigned int pos); Вход: неявный аргумент a pos - позиция того бита, который будет проверен (начиная с 0) Возврат: 1, если бит находящийся в позиции pos установлен, в противном случае 0 Пример: if(a.testbit (512)) //... Функция: установка двоичного разряда объекта LINT в 0 Синтаксис: const LINT& LINT::clearbit (const unsigned int pos); Вход: неявный аргумент a pos - позиция того бита, который будет установлен (начиная с 0) Возврат: указатель на а с нулевым битом в позиции pos Пример: a.clearbit (512); f
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 315 функция: обмен значениями двух объектов LINT Синтаксис: const LINT& LINT::fswap (LINT& b); Вход: неявный аргумент а второй аргумент b (значение, которое будет меняться на а) Возврат: указатель на неявный аргумент со значением b Пример: a.fswap (b); а и b обмениваются значениями 14.2. Теория чисел ; В отличие от арифметических функций, следующие теоретико- числовые функции-члены не перезаписывают первый неявный аргумент с результатом. Причиной этому является то, что при использовании более сложных функций, как показала практика, обычно не нужно перезаписывать аргумент, как это происходит в случае с простыми арифметическими функциями. Результаты следующих функций соответственно возвращаются как значения, а не как указатели. Функция: вычисление наибольшего целого, меньшего или равного логарифму по основанию 2 от объекта LINT Синтаксис: ’ const unsigned int LINT::ld (void); Вход: неявный аргумент a Возврат: целая часть log2 a Пример: i = a.ld();
316 Криптография на Си и C++ в действии Функция: вычисление наибольшего общего делителя двух объектов LINT Синтаксис: const LINT LINT::gcd (const LINT& b); Вход: неявно выраженный аргумент a второй аргумент b Возврат: НОД(а, b) Пример: с = a.gcd (b); Функция: вычисление обратного значения по модулю п Синтаксис: const LINT LINT::inv (const LINT& n); Вход: неявный аргумент a модуль n Возврат: обратная величина по модулю п (если результат равен нулю, то gcd(a, п) >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); Вход: неявный аргумент a, второй аргумент b Выход: множитель u в представлении НОД(а, b) sign_u - знак и множитель v в представлении НОД(а, Ь) sign_v - знак v Возврат: НОД(а, Ь) Пример: g = a.xgcd (b, u, sign_u, v, sign_v);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 317 Функция: наименьшее общее кратное двух объектов LINT. Синтаксис: const LINT LINT::lcm (const LINT& b); Вход: неявный аргумент a, второй аргумент b Возврат: HOK (a, b) Пример: c = a.lcm (b); Функция: решение системы линейных сравнений х = a mod m, х = b mod п Синтаксис: const LINT LINTr.chinrem (const LINT& m, const LINT& b, const LINT& n); Вход: неявный аргумент а, модуль m, аргумент b, модуль n Возврат: решение x системы сравнений, если все в порядке (Get_Waming_Status() == EL1NTERR означает, что произошло переполнение или сравнения не имеют общего решения) Пример: х = a.chinrem (m, b, n); Функция-друг chinrem (const int noofeq, LINT** coeff) получает coeff - вектор указателей на объекты LINT, которые передаются как коэф- ’< фициенты aj, mlt а2, т2> а3, т3> ... системы линейных сравнений x^at mod mif i=l, ..., noofeq (см. Приложение В) Функция: вычисление символа Якоби двух объектов LINT Синтаксис: const int LINT::jacobi (const LINT& b); Вход: неявный аргумент а, аргумент b Возврат: Символ Якоби от двух входных значений Пример: i = a.jacobi (b);
318 Криптография на Си и C++ в действии Функция: вычисление целой части от квадратного корня Синтаксис: const LINT LI NT::root (void); Вход: неявный аргумент a Возврат: целая часть от квадратного корня Пример: с = a.root (); Функция: вычисление квадратного корня по модулю простого числа р Синтаксис: const LINT LI NT::root (const LINT& p); Вход: неявный аргумент а, простой модуль p > 2 Возврат: квадратный корень из а, если а - квадратичный вычет по модулю р, в противном случае 0 (Get_Warning_Status() == E_LINT_ERR показывает, что а не является квадратичным вычетом по модулю р) Пример: с = a.root (р); Функция: вычисление квадратного корня объекта LINT по модулю а простого произведения р • q Синтаксис: const LINT LINT::root (const LINT& p, const LINT& q); Вход: неявный аргумент a простой модуль p > 2, простой модуль q > 2 Возврат: квадратный корень из а, если а - квадратичный вычет по модулю PQ> в противном случае 0 (Get_Warning_Status() == E_LINT_ERR показывает, что а не является квадратичным вычетом по модулю p*Q) Пример: с = a.root (р, q);
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 319 Функция: проверка, является ли объект LINT квадратом Синтаксис: const int LINTr.issqr (void); Вход: кандидат а как неявный аргумент Возврат: квадратный корень из а, если а является квадратом, в противном случае 0, если а == 0 или а не является квадратом Пример: if(0 == (г = a.issqr ())) И... Функция: вероятностная проверка объекта LINT на простоту Синтаксис: const int LINT::isprime (void); Вход: кандидат а как неявный аргумент Возврат: 1, если а “вероятно” простое, в противном случае 0 Пример: if(a.isprime ()) //... Функция: Разложение CLINT-объекта в виде а = 2еb Синтаксис: const int LINT::twofact (LINT& b); Вход: неявный аргумент а Выход; b (нечетная часть а) Возврат: показатель степени четной части а Пример: е = a.twofact (b);
320 Криптография на Си и C++ в действии 14.3. Потоковый ввод/вывод объектов LINT Классы, содержащиеся в стандартных библиотеках языка C++, такие как istream и ostream, являются абстракциями устройств ввода/вывода, полученных из базового класса ios. Класс iostreami в свою очередь, получен из istream и ostream, и он позволяет писати и читать из них объекты1. Ввод и вывод происходит с помощьк| операторов поместить (insert) и извлечь (extract) “«” и ”»’] (см. [Teal], глава 8). Они возникают при перегрузке оператором сдвига, например, в выражении I ostream& ostream::operator« (int i); I istream& istream::operator>> (int& i); I в котором они разрешают вывод и ввод, соответственно, целым значений через следующие выражения: I cout«i; cin »i; I В качестве специальных объектов классов ostream и istream, al именно 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++, в которой имена класса, известные ранее, получили префикс basics Настройка этого идет в самой стандартной библиотеке, в которой к ранее использовавшимся име- нам классов можно обращаться через соответствующие объявления типов (typedef) (см. [KSchL Глава 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 (lln.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++ и его функциями-членами для того, чтобы оп- ределить наши собственные функции форматирования, подходящие для LINT, с целью управления форматом вывода объектов LINT. Л Далее мы создадим манипуляторы, которые организуют настройку формата вывода объектов LINT настолько просто, как это сделано для стандартных типов, определенных в C++. Ключевым моментом в создании форматированного вывода объек- тов LINT является возможность установки спецификаций формата, которые будут обрабатываться оператором Мы рассмотрим механизм, обеспечиваемый классом ios (для более подробного рас- смотрения см. [Teal], Глава 6, и [Р1а2], Глава 6), у которого функция- 11 _ 1AQ7
322 Криптография на Си и C++ в действии член xalloc() в объектах классов, производных от ios, выделяет пе- ременную состояния типа long и возвращает ее индекс того же типа. Этот индекс хранится в переменной flagsindex. С помощью нее функцию-член ios::iword() можно использовать для того, чтобы по- лучить доступ по чтению и записи к выделенной переменной управ- ления выводом (иначе, переменной состояния) (см. [Р1а2], стр. 125). Для полной уверенности, что это происходит до вывода объекта LINT, мы определяем в файле flintpp.h класс Lintlnit следующим образом: class Lintlnit I < public: I 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 { J public: If... enum {
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 323 lintdec = 0x10, lintoct = 0x20, linthex = 0x40, lintshowbase = 0x80, lintuppercase =0x100, lintbin = 0x200, lintshowlength = 0x400 }; n... friend Lintlnit::Lintlnit (void); //... private: H... static long flagsindex; static Lintlnit setup; H... }; Задание переменной setup как static означает, что эта переменная существует только один раз для всех объектов LINT, и, таким обра- зом, связанный конструктор Lintlnit() вызывается только один раз. Здесь мы хотим ненадолго остановиться и рассмотреть всю проде- ланную нами работу. Установку формата вывода можно с тем же успехом организовать с помощью переменной состояния, с которой как с членом класса LINT намного проще было бы иметь дело. Ос- новным преимуществом метода, который мы выбрали, является то, что вывод формата можно установить для каждого потока вывода отдельно и независимо друг от друга, (см. [Р1а2], страница 125), че- го нельзя сделать, используя внутреннюю для класса LINT пере- менную состояния. Это организуется с помощью возможностей класса ios, механизмы которого нам пригодятся для подобных целей. Теперь, после того как были рассмотрены необходимые замечания, мы сможем определить функции состояния как члены класса LINT. Они отражены в Таблице 14.1. Мы рассмотрим пример реализации функций состояния для функ- ции LINT::setf(), которая возвращает текущее значение состояния переменной типа long с ссылкой на поток вывода:
324 Криптография на Си и C++ в действии Таблииа 14.1 Функция состояния Пояснения Функиии- состояния класса LINT и результат их действия static long LINT::flags (void); static long LINT::flags (ostream&); Считывает переменную состояния, относящуюся к cout Считывает переменную состояния, относящуюся к заданному потоку вывода static long LINT::setf (long); Устанавливает отдельные биты переменной состояния cout и возвращает предыдущее значение static long LINT::setf (ostream&, long); Устанавливает отдельные биты переменной состояния заданного потока и возвращает предыдущее значение static long LINT::unsetf (long); Восстанавливает отдельные биты переменной состояния cout и возвращает^ предыдущее значение |И static long LINT::unsetf (ostream&, long); Восстанавливает отдельные биты переменной состояния заданного потока и возвращает предыдущее значение static long LI NT:: restore! (long); Устанавливает переменную состояния cout и возвращает предыдущее значение static long LINT::restoref (ostream&, long); Устанавливает переменную состояния заданного потока и возвращает предыдущее значение long LINT::setf (ostream& s, long flag) i.. long t = s.iword (flagsindex); // Флаги для основания числового представления взаимоисключающие: if (flag & LINT: Jintdec) { s.iword (flagsindex) = (t & ~LINT::linthex & ~LINT::lintoct & -LINT"lintbin) | LINT::lintdec; flag *= LINT::lintdec; } if (flag & LINT::linthex)
ГЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 325 s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::lintoct & ~LINT::lintbin) | LINT::linthex; flag л= LINT::linthex; } if (flag & LINT::lintoct) t s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::linthex & ~LINT::lintbin) | LINT::lintoct; flag л= LINT::lintoct; } if (flag & LINT::lintbin) { s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::lintoct & ~LINT::linthex) | LINT::lintbin; flag A= LINT::lintbin; } // Все остальные флаги являются взаимно совместимыми s.iword (flagsindex) |= flag; ..-и • • return t; } С помощью этой и оставшихся функций в Таблице 14.1 мы можем далее определить разные форматы вывода. Первоначально стан- дартный формат вывода представляет собой значение объекта LINT как шестнадцатиричное число в виде строки символов, которая ; ' 0 занимает столько строк на экране, сколько требуется для вывода всех цифр объекта LINT. В добавочной строке число цифр объекта LINT размещено слева от края. Были созданы следующие дополни- тельные режимы вывода объекта LINT: ‘Эсгдо 1. Основание представления цифр. Стандартным представлением цифр объектов LINT является шест- надцатиричное, а представлением длины - десятичное. Такие умолчания для объектов LINT могут быть установлены для стан- дартного потока вывода cout с определенным основанием системы счисления base с помощью вызова
326 Криптография на Си и C++ в действии LINT::setf (LINT::base); а на заданный поток LlNT::setf (ostream, LINT::base); где base может принимать одно из значений: linthex, lintdec, lintoct, lintbin которые обозначают соответствующий формат вывода. Например вызов LINT::setf(lintdec) устанавливает вывод формата цифр в деся- тичной форме. Основание системы счисления для представления длины может быть задано функцией ios::setf (ios::iosbase); с iosbase = hex, dec, oct. 2. Отображение префикса для числового представления Для объекта LINT установкой по умолчанию является отображение с префиксом, показывающим как он представлен. Следующие вызовы LINT::unsetf (LINT::lintshowbase); LINT::unsetf (ostream, LINT::lintshowbase); меняют эту установку. 3. Отображение шестнадцатиричных цифр в верхнем регистре По умолчанию установлено отображение шестнадцатиричных цифр и префикса Ох для шестнадцатиричного представления в нижнем регистре a b с d е f. Однако вызов LINT::setf (LINT::lintuppercase); LINT::setf (ostream, LINT::lintuppercase); меняет их для того, чтобы они превратились в префикс ОХ и боль- шие буквы А В С D Е F. 4. Отображение длины объекта LINT По умолчанию установлено отображение двоичной длины объекте! LINT. Это можно изменить, вызвав LINT::unsetf (LINT::lintshowlength); LINT::unsetf (ostream, LINT::lintshowlength); для того чтобы длина не отображалась.
Г ЛАВА 14. Открытый интерфейс LINT: члены и друзья класса 5. Восстановление переменной состояния 327 Переменную состояния для форматирования объекта LINT можно восстановить в предыдущее значение oldflags с помощью вызова двух функций LINT::unsetf (ostream, LINT::flags(ostream)); LINT::setf (ostream, oldflags); Вызовы этих двух функций собраны в перегруженной функции restoref(): LINT::restoref (flag); LINT::restoref (ostream, flag); Флаги можно объединить, как показано в вызове LINT::setf (LINT::bin | LINT::showbase); Однако это разрешено только лишь для флагов, которые не являют- ся взаимоисключающими. Функции вывода, которые в конечном итоге и генерируют задан- ный формат объекта LINT, являются расширением оператора ostream& operator <<(ostream& s, LINT In), о котором в общих чертах говорилось ранее. Он оценивает переменную состояния потока вы- вода и генерирует соответствующий вывод. Для данного оператора применяется вспомогательная функция Iint2str(), содержащаяся в файле flintpp.cpp, которая в свою очередь вызывает функцию xclint2strj() для того, чтобы представить числовое значение объекта LINT как строку символов: ostream& operator« (ostream& s, const LINT& In) { USHORT base = 16; long flags = LINT::flags (s); char* formattedjint; if (lln.init) LINT::panic (E_LINT_VAL, "ostream operator«", 0, __LINE_); if (flags & LINT::linthex) { base = 16;
Криптография на Си и 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 -flags = s.flags (); //Получение текущего состояния s.setf (ios::dec); //установка флага для десятичного отображения s « endl « Id (In) « " bit" « endl; //восстановление предыдущего состояния s.setf (_flags); } ‘ к; U1 return s; } 14.3.2. Манипуляторы Опираясь на предыдущие механизмы, в данном разделе мы хотим достичь более подходящих вариантов для управления форматом вывода для объектов LINT. Для этого мы используем .манипуля- торы, которые размещены прямо в потоке вывода, и, их эффект аналогичен тому, что мы получали при вызове ранее рассмотренных функций состояния. Манипуляторы являются адресами функций, для которых существует специальный вид оператора поместить (“«”), который в свою очередь в качестве аргумента принимает указатель на функцию. Для примера мы рассмотрим следующую функцию: ostream& LintHex (ostream& s) г t LINT::setf (s, LINT::linthex); return s; 1Ы } Данная функция вызывает функцию состояния setf(s, LINT::linthex) в контексте заданного потока вывода ostream& s и, таким образом, задает формат вывода объектов LINT в форме шестнадцатиричных чисел. Имя функции LintHex без круглых скобок рассматривается как указатель на функцию (см. [Lipp], страница 202), и он может быть направлен в поток вывода как манипулятор с помощью опера- тора “«” ostream& ostream::operator« (ostream& (*pf)(ostream&))
10 Криптография на Си и C++ в действи return (*pf)(*this); } определенного в классе ostream: LINT a ("0x123456789abcdef0"); cout « LintHex « a: ostream s; s « LintDec « a; Функции-манипуляторы LINT работают по приведенному шаблону как стандартные манипуляторы dec, hex, oct, flush, и endl. Оператор “«” в библиотеке языка C++, например пулятор функции LintHex() или LintDecQ просто вызывает мани подходящий момент Манипуляторы обеспечивают установку флагов состояния, принад лежащих потокам вывода cout и, соответственно, s. Перегруженный оператор “«” вывода объектов LINT переносит объекта а типа LINT в запрошенную форму. представление Все установки формата вывода объектов LINT могут быть вы пол йены с помощью манипуляторов, представленных в Таблице 14.2. Таблииа 14.2. Манипулятор Результат вывода значений LINT Манипуляторы LINT и результат LintBin как числа в бинарном виде их выполнения LintDec как числа в десятичном виде LintHex как числа в шестнадиатиричном виде LintOct как числа в восьмеричном виде LintLwr с символами нижнего регистра a, b, с, d, е, ( для шестнадцатиричного представления LintUpr с символами верхнего регистра А, В, С, D, Е, F для шестнадцатиричного представления LintShowbase с префиксом для числового представления (Ох или ОХ в шестнадцатиричном и 0Ь - бинарном) LintNobase без префикса для числового представления LintShowlength с отображение числа разрядов LintNolength без отображения числа разрядов В добавление к манипуляторам в таблице 14.2., которым ется аргумент, доступны следующие манипуляторы: LINT_omanip<int> SetLintFlags (int flags) в
ГЛАВА 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. Табл и на 14.3. Флаги LINT для форма тирования Флаг Значение lintdec 0x010 вывода lintoct 0x020 linthex 0x040 lintshowbase 0x080 lintuppercase 0x100 lintbin 0x200 • 1 ю ;; . Э * lintshowlength 0x400 -С по Следующим примером мы внесем ясность в применение самих ’ •’! а*} . функций форматирования и манипуляторов: ' -f' • / н? Ж' #include "flintpp.h" #include <iostream.h> #include <iomanip.h> main() { LINT n ("0x0123456789abcdef"); И число LINT с основанием 16 long deflags = LINT::flags(); И запомним флаги cout « "Представление по умолчанию:" « n « endl;
332 Криптография на Си и C++ в действии LINT::setf (LINT::linthex | LINT::lintuppercase); cout « "Шестнадцатиричное представление символов в верхнем регистре:" « n « endl; cout « LintLwr « " Шестнадцатиричное представление символов в нижнем регистре:" « n « endl; cout « LintDec « "десятичное представление:" « n « endl; cout « LintBin « "двоичное представление:" « n « endl; cout « LintNobase « LintHex; cout «"представление без префик