Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Практическая криптография

..pdf
Скачиваний:
6
Добавлен:
12.11.2023
Размер:
16.23 Mб
Скачать

152

Глава 9. Проблемы реализации. Часть I

ориентирована в первую очередь на обеспечение безопасности. Из этого следует, что реализовать безопасную систему невозможно. Мы не знаем, как это сделать, и пока не видим никого, кто бы это знал. Реальные системы вклю­ чают в себя массу компонентов, которые никогда не были ориентированы на обеспечение безопасности, поэтому достигнуть того уровня безопасности, ко­ торый нам нужен, просто невозможно. Так, может быть, стоит махнуть на все рукой? Вовсе нет. Когда мы разрабатываем криптографическую систему, то прилагаем все усилия, чтобы гарантировать безопасность хотя бы нашей части системы. На первый взгляд это может напомнить психологию эгои­ ста, которого беспокоит только его маленький мирок. Это совсем не так: нас действительно беспокоят другие части системы; мы просто не в состоянии контролировать их разработку. Потому-то мы и решили написать эту кни­ гу, чтобы другие тоже осознали коварную природу безопасности и поняли, насколько важно правильно выполнять свою работу.

Еще одна причина того, что криптографическая часть системы долж­ на быть реализована правильно, уже рассматривалась в одной из предыду­ щих глав: атаки на криптографические алгоритмы особенно опасны потому, что могут остаться незамеченными. Если злоумышленнику удастся взломать криптографический алгоритм, вы вряд ли это заметите. Это можно сравнить с вором, у которого есть все ключи от вашего дома. Если вор предпримет необходимые меры предосторожности, как вы узнаете, что он у вас побывал?

Наша долгосрочная цель состоит в том, чтобы разрабатывать безопасные компьютерные системы. Для достижения этой цели каждый должен выпол­ нять то, что от него требуется. Наша работа, которой и посвящена эта кни­ га, — обеспечить безопасность компонентов, отвечающих за криптографию. Кто-то другой должен обеспечить безопасность остальных частей системы. Мы не знаем, как это делать, но, вероятно, это знают другие разработчики (а, может быть, когда-нибудь узнаем и мы). Пока этого не случится, без­ опасность системы в целом будет ограничена надежностью ее самого слабого звена, и мы сделаем все возможное, чтобы этим звеном никогда не стала криптография.

Нельзя не упомянуть и еще одну причину, по которой так важно правиль­ но проектировать криптографические системы. Операционная система функ­ ционирует на отдельном компьютере. Криптографические системы, в свою очередь, часто применяются в коммуникационных протоколах, предназна­ ченных для обмена данными между множеством компьютеров. Обновление операционной системы, установленной на отдельном компьютере, является вполне осуществимой задачей и на практике выполняется относительно ча­ сто. Изменение коммуникационных протоколов в сети — это сущий кошмар. Не зря же многие сети все еще работают с технологиями 70-х и 80-х годов прошлого века! Мы должны учитывать, что любая разработанная крипто­

9.1 Создание правильных программ

153

графическая система в случае ее широкого распространения будет использо­ ваться на протяжении еще 30-50 лет. Надеемся, что к тому времени другие части системы достигнут гораздо более высокого уровня безопасности.

9.1 Создание правильных программ

Источник всех проблем реализации кроется в том, что мы, IT-специали­ сты, не знаем, как написать правильную программу или модуль. (“Правиль­ ная” программа — это программа, которая ведет себя именно так, как описа­ но в ее спецификациях.) Существует несколько причин того, почему нам так трудно написать правильную программу.

9.1.1Спецификации

Первая причина касается отсутствия спецификаций. Для большинства программ не существует четкого описания того, что именно они должны де­ лать. Если у программы нет спецификаций, мы даже не можем проверить, правильная она или нет. Сама концепция правильности для таких программ теряет свою определенность.

Многие программные проекты сопровождаются документом, называемым функциональной спецификацией. Теоретически это и есть спецификация про­ граммы. На практике, однако, этот документ либо не существует, либо явля­ ется неполным, либо описывает вещи, не относящиеся к поведению програм­ мы. Не имея четкой спецификации, нечего и надеяться получить правильную программу.

Процесс создания спецификаций можно разделить на три этапа.

Требования. Это неформальное описание того, для чего предназна­ чена программа. Этот документ описывает скорее “что я могу сде­ лать с помощью этой программы”, нежели то, “как я могу это сделать”.

Вбольшинстве случаев требования несколько расплывчаты и не содер­ жат мелких деталей. Они сконцентрированы на программе в целом.

Ф ункциональная спецификация. Это подробное и исчерпывающее описание поведения программы. В функциональной спецификации должны содержаться только те элементы программы, поведение кото­ рых может быть оценено “из внешнего мира”.

Прежде чем помещать какой-либо элемент в функциональную специфи­ кацию, подумайте, можно ли разработать тест для готовой программы, который будет определять, функционирует ли этот элемент должным образом. Тест должен использовать только внешнее поведение програм­ мы, а не ее внутренние механизмы. Если такой тест создать нельзя,

154

Глава 9. Проблемы реализации. Часть I

соответствующий элемент не должен быть указан в функциональной спецификации.

Функциональная спецификация должна быть полной. Другими словами, она должна содержать описание абсолютно всех функциональных элементов программы. Все, что не было занесено в функциональную спецификацию, не должно быть реализовано.

Для большей наглядности функциональную спецификацию можно представить себе в виде основы для тестирования готовой програм­ мы. Все элементы, указанные в функциональной спецификации, могут

идолжны быть протестированы.

План реализации. У этого документа много названий. План реализа­ ции описывает внутреннее поведение программы. В нем указаны все те элементы, которые не могут быть протестированы извне. Хороший план реализации зачастую разбивает программу на несколько модулей и опи­ сывает функциональность каждого из них. Описание модуля, в свою очередь, можно рассматривать как составление требований к модулю

споследующим созданием функциональной спецификации и плана ре­ ализации.

Из всех перечисленных документов наиболее важной, несомненно, являет­ ся функциональная спецификация. Именно она служит основой для тестиро­ вания готовой программы. Иногда для разработки программы может вполне хватить неформальных требований или плана реализации, представляющего собой лишь несколько заметок на клочке бумаги. Но при отсутствии функ­ циональной спецификации мы даже не сможем описать, чего мы достигли по окончании разработки программы.

9.1.2 Тестирование и исправление

Вторая проблема написания правильных программ касается повсеместно распространенного метода разработки “протестировать и исправить”. Про­ граммисты пишут программу и затем тестируют ее на предмет того, правиль­ но ли она работает. Если она не работает или работает неправильно, програм­ мисты исправляют найденные ошибки и снова тестируют программу. Всем известно, что такой подход отнюдь не приводит к появлению правильной про­ граммы. Он позволяет лишь получить программу, которая будет работать (а точнее, возможно, будет работать) в наиболее стандартных ситуациях.

В 1972 году Эдсгер Дейкстра (Edsger Dijkstra) в одной из своих речей отметил, что тестирование может показать только наличие ошибок, а не их отсутствие [22]. Подмечено как нельзя точно. В идеале мы хотели бы со­ здавать такие программы, правильность которых могла бы быть доказана.

9.1 Создание правильных программ

155

К сожалению, современные методы доказательства правильности программ не способны справиться и с повседневными задачами программирования, не говоря уже о целых проектах.

Пока что специалисты по компьютерным технологиям не знают, как ре­ шить эту проблему. Может быть, когда-нибудь мы научимся доказывать пра­ вильность программы. Возможно, нам просто нужна более обширная и тща­ тельная инфраструктура и методология тестирования. Но, даже не имея на руках полного решения, мы определенно способны сделать все возможное с помощью тех средств, которые у нас уже есть.

Существует несколько простых правил отслеживания ошибок, которые содержатся в каждом хорошем учебнике по программной инженерии.

Если вы нашли ошибку, реализуйте тест, который будет обнаруживать эту ошибку. Убедитесь, что он действительно ее обнаруживает. Затем исправьте ошибку и убедитесь, что тест больше не обнаруживает ее. Впоследствии применяйте этот тест ко всем будущим версиям програм­ мы, чтобы проверить, не появится ли указанная ошибка снова.

Найдя ошибку, подумайте, чем она вызвана. Нет ли в программе других мест, в которых может находиться аналогичная ошибка? Проверьте все эти места.

Ведите учет всех найденных ошибок. Простой статистический анализ обнаруженных ошибок может показать, какая часть программы являет­ ся самой проблемной, ошибки какого типа встречаются наиболее часто и т.п. Это необходимо для системы контроля качества.

Перечисленные правила даже нельзя назвать необходимым минимумом, однако выбирать все равно не из чего. Книг, посвященных качеству про­ граммного обеспечения, слишком мало, и все они в чем-то не согласуются друг с другом. Многие из них представляют конкретную методологию раз­ работки программного обеспечения как единственное решение, а мы всегда с подозрением относимся к схемам, которые “лечат от всех болезней”. Истина почти всегда находится где-то посередине.

9.1.3 Халатное отношение

Третья проблема — это невероятно халатное отношение к программному обеспечению большинства людей, работающих в компьютерной индустрии. Ошибки в программах воспринимаются как нечто само собой разумеющееся. Если ваш текстовый процессор неожиданно “упадет” и уничтожит все, что вы успели сделать за день, это будет воспринято как вполне обыденная ситуа­ ция. Зачастую всю вину пытаются переложить на пользователя: “ Вы должны

156

Глава 9. Проблемы реализации. Часть I

были почаще сохранять документ”. Производители программного обеспече­ ния постоянно выпускают продукты, содержащие массу известных ошибок. Было бы полбеды, если бы они продавали только компьютерные игры, но сегодня от программного обеспечения зависит наша работа, наша экономика и — все больше и больше — наша жизнь. Если производитель машин обнару­ жит дефект в модели машины после ее поступления в продажу, он отзовет все экземпляры этой модели и исправит найденный дефект. Компании, за­ нимающиеся разработкой программного обеспечения, поступают по-другому, всячески заявляя об ограничении своей ответственности во всевозможных лицензиях. В любой другой отрасли промышленности этого бы не спустили с рук. Такое халатное отношение означает отсутствие каких-либо серьезных попыток разработать правильное программное обеспечение.

9.1.4 Так что же нам делать?

Даже и не думайте, что вам будет достаточно лишь хорошего программи­ ста, нескольких обзоров кода сторонними аналитиками, процесса разработки, сертифицированного по ISO 9001, обширного тестирования или даже комби­ нации всего этого. Действительность более сурова. Программное обеспечение слишком строптиво, чтобы его можно было приручить с помощью нескольких правил и процедур.

В качестве поучительного примера можно привести самую лучшую си­ стему контроля качества в мире — авиационную промышленность. Каж­ дый человек, работающий в этой отрасли, связан с обеспечением безопас­ ности. Практически каждая операция выполняется согласно строгим пра­ вилам и процедурам. На случай отказа элементов системы предусмотрены многочисленные резервные компоненты. Каждый болт и каждая гайка само­ лета проходят тщательные технические испытания на пригодность, прежде чем их вообще разрешат использовать. Всякий раз, когда механик подходит

ксамолету хотя бы с самой безобидной отверткой, его работа проверяется

иутверждается руководителем инженерно-авиационной службы. Любое из­ менение фиксируется самым тщательным образом. Каждое незначительное происшествие тщательно исследуется на предмет выявления всех возмож­ ных причин, которые затем исправляются. Эта беспрецедентная погоня за качеством оборачивается огромными расходами. Производство самолета об­

ходится на порядок дороже, чем если бы вы просто передали его чертежи в обычную конструкторскую фирму. Но в то же время эта погоня за каче­ ством оказывается исключительно эффективной.

Сегодня летать самолетом — совершенно привычное дело, а ведь даже незначительная ошибка в подобной машине была бы равносильна смерти. В машине, в которой нельзя просто нажать на тормоза и остановиться, если

9.2 Создание безопасного программного обеспечения

157

что-нибудь пойдет не так.

В машине, которая сможет вернуться на землю

и при этом не разбиться,

только совершив точную и мастерскую посадку

на один из специально подготовленных участков, которых так немного на нашей планете. Авиационная индустрия удивительно преуспела в обеспече­ нии безопасности полетов. Было бы хорошо, если бы у нее поучились и все остальные.

Возможно, написание правильного программного обеспечения действи­ тельно стбит на порядок дороже, чем на него привыкли тратить сейчас. Тем не менее, учитывая расходы, которыми оборачиваются ошибки программно­ го обеспечения для современного общества, такое решение показало бы себя весьма эффективным в долгосрочной перспективе.

9.2Создание безопасного программного обеспечения

До сих пор речь шла только о создании правильных программ. К сожале­ нию, одного лишь правильного программного обеспечения недостаточно для построения системы безопасности. Программное обеспечение должно быть еще и безопасным.

В чем же различие между правильным и безопасным программным обес­ печением? Правильное программное обеспечение обладает заявленной функ­ циональностью: если вы щелкнете на кнопке А, случится событие Б. К без­ опасному программному обеспечению выдвигается еще одно требование: недо­ статочность функциональности. Что бы ни делал злоумышленник, он не должен выполнить операцию X . Это различие поистине можно назвать фун­ даментальным; можно протестировать программу на предмет функциональ­ ности, но никак не на предмет недостаточности функциональности. Аспекты безопасности программного обеспечения не могут быть протестированы ка­ ким-либо эффективным образом. Все это делает разработку безопасного про­ граммного обеспечения гораздо более сложной, нежели создание правильного программного обеспечения. Из этого следует вывод:

Стандартные методы реализации совершенно не подходят для написания безопасного кода.

В действительности мы не знаем, как создавать безопасный код. Пробле­ ма качества программного обеспечения настолько обширна, что на ее рас­ смотрение понадобилось бы несколько отдельных книг. Мы не разбираемся в этой теме настолько хорошо, чтобы написать их, но знаем наиболее распро­ страненные проблемы реализации, касающиеся криптографии. Именно о них и пойдет речь в оставшейся части главы.

158

Глава 9. Проблемы реализации. Часть I

Прежде чем переходить к обсуждению этих проблем, давайте проясним нашу точку зрения: если вы не собираетесь вложить все свои силы в разработку безопасной системы, не стоит и браться за криптографию. Разра­ ботка криптографических систем может быть интересным занятием, однако, небрежно относясь к их реализации, вы лишь впустую потратите время.

9.3Как сохранить секреты

Все, кто работает с криптографией, имеют дело с секретами. А секреты для того и нужны, чтобы сохранять их в тайне. Это значит, что программное обеспечение, связанное с секретной информацией, должно гарантировать, что ее утечка невозможна.

Работая с безопасным каналом общения, мы имеем дело с двумя типами секретов: ключи и данные. И те и другие являются временными; мы не хо­ тим хранить их слишком долго. Данные хранятся только на протяжении того времени, пока мы обрабатываем каждое сообщение. Ключи хранятся только на протяжении времени существования безопасного канала общения. В этой главе обсуждаются только временные секреты. Как обеспечить хранение дол­ госрочных секретов, рассматривается в главе 22, “Хранение секретов”.

Временные секреты хранятся в оперативной памяти компьютера. К со­ жалению, память большинства современных компьютеров не обладает нуж­ ной безопасностью. Ниже описываются типичные проблемы, сопровождаю­ щие хранение секретов.

9.3.1 Уничтожение состояния

Основное правило написания систем безопасности гласит: уничтожайте всю информацию, как только она вам больше не понадобится. Чем дольше она будет храниться в памяти компьютера, тем вероятнее, что до нее добе­ рется кто-нибудь посторонний. Более того, не забывайте уничтожать данные прежде, чем вы потеряете контроль над носителем, используемым для их хранения. Касательно временных секретов это означает очищение соответ­ ствующих областей памяти.

На первый взгляд кажется, что соблюсти это правило проще простого. Между тем оно приводит к возникновению огромного числа проблем. Если вы самостоятельно пишете весь текст программы на языке С, то сможете по­ заботиться об уничтожении данных и сами. Если же вы пишете библиотеку для использования другими людьми, то попадаете в зависимость от главной программы, которая должна сообщать вам о том, что то или иное состоя­ ние больше не понадобится. Например, при закрытии соединения криптогра­ фическая библиотека должна получить уведомление о том, что она может

9.3 Как сохранить секреты

159

уничтожить состояние сеанса безопасного канала общения. Библиотека да­ же может содержать специальную функцию для уничтожения состояния, но что, если разработчик приложения не станет утруждать себя вызовом еще одной функции? В конце концов, программа будет прекрасно работать и без обращения к этой функции.

В некоторых объектно-ориентированных языках программирования ре­ шить эту проблему несколько проще. В C++ у каждого объекта есть деструк­ тор, который может автоматически уничтожать его состояние. Это стандарт­ ный прием, применяемый при написании систем безопасности на C++. Если главная программа ведет себя правильно и уничтожает все ненужные объек­ ты, деструкторы автоматически очистят соответствующие области памяти. Язык C ++ гарантирует, что при обработке исключения будут корректно уни­ чтожены все объекты стека, однако уничтожением объектов, хранящихся в динамической памяти (“куче”), должна заниматься сама программа. Ес­ ли для завершения работы программы вызывается функция операционной системы, она может не удосужиться даже пройти по стеку вызовов. Вам при­ дется самим обеспечивать уничтожение данных, даже если программа вскоре должна завершить работу. В конце концов, операционная система не дает ни­ каких гарантий того, что данные будут стерты из памяти в самое ближайшее время. Некоторые операционные системы вообще не утруждают себя очист­ кой освободившейся памяти, когда передают ее следующему приложению.

Даже если вы проделаете все необходимые операции по уничтожению объектов, ваши усилия могут быть сведены на нет. Некоторые компилято­ ры слишком усердствуют, стараясь все и всегда оптимизировать. Типичная функция, имеющая отношение к системе безопасности, выполняет несколь­ ко вычислений в локальных переменных и затем пытается уничтожить их, очистив соответствующие области памяти. В языке С для этого часто ис­ пользуют функцию memset. Хорошие компиляторы оптимизируют функцию memset, заменяя при трансляции вызов этой функции ее телом, что повышает скорость работы программы. Но некоторые компиляторы в своем стремлении к оптимизации заходят чересчур далеко. Они определяют, что уничтожаемая переменная или массив больше не будут использоваться в программе, и вооб­ ще пропускают функцию memset. Это еще больше повышает скорость рабо­ ты программы, однако способно изменить ее поведение самым неожиданным образом. Нередко код программы выдает данные, случайно обнаруженные им в оперативной памяти. Если высвобожденная, но не очищенная память передается какой-нибудь библиотеке, использование последней может при­ вести к утечке данных, которые тут же попадут в руки злоумышленника. Поэтому не забывайте проверять код, который генерирует ваш компилятор, и убеждаться в том, что секретные данные действительно удаляются из па­ мяти компьютера.

160

Глава 9. Проблемы реализации. Часть I

Ситуация еще более усложняется в языках наподобие Java. Здесь все объекты находятся в динамической памяти, которая периодически подверга­ ется очистке посредством сборщика мусора (garbage collector). Это означает, что метод Finalize (аналог деструктора в C ++) не вызывается до тех пор, пока сборщик мусора не обнаружит, что объект больше не используется. Ни­ каких спецификаций относительно того, как часто запускается сборщик мусо­ ра, не существует. Вполне вероятно, что секретные данные остаются в памя­ ти на протяжении достаточно долгого времени. Использование обработки ис­ ключений значительно затрудняет очищение памяти вручную. Если програм­ ма выдает исключение, она проходит по стеку вызовов, не давая программи­ сту никакой возможности вставить свой собственный код. Единственное, что можно было бы сделать в данной ситуации, — это представить каэ/сдую функ­ цию в виде большого блока try. Разумеется, данное решение слишком урод­ ливо и непрактично. Оно также должно было бы применяться на протяжении абсолютно всей программы, что сделало бы невозможным создание хорошей библиотеки безопасности для Java. В процессе обработки исключений Java спокойно проходит по стеку вызовов, выбрасывая ссылки на объекты и не уничтожая при этом самих объектов. В этом отношении Java действительно оказывается не на высоте. Самое лучшее решение, которое мы смогли при­ думать на данный момент, — это гарантировать запуск методов Finalize по крайней мере при завершении работы программы. Для этого метод main дол­ жен содержать операторы try -fin a lly . Код, содержащийся в блоке finally, должен инициировать принудительную очистку памяти, дав указания сбор­ щику мусора, чтобы тот попытался завершить все методы F in a lize. (См. до­ кументацию к функциям System.gc() и S ystem .ru n ln itialization O .) Дан­ ный прием тоже не гарантирует, что методы Finalize действительно будут запущены, но это лучшее, что можно сделать.

Чего нам действительно не хватает — так это поддержки от самого языка программирования. В C ++ есть хотя бы теоретическая возможность напи­ сать программу, уничтожающую содержимое всех объектов, которые уже не нужны. К сожалению, другие особенности этого языка делают его выбор крайне неудачным для написания систем безопасности. В Java уничтожить содержимое объекта явно практически невозможно. Было бы хорошо, если бы мы могли объявлять такие переменные, как “sensitive” (“требующие осо­ бого обращения”), что гарантировало бы уничтожение их содержимого. Еще лучше, если бы у нас был язык программирования, который бы всегда уни­ чтожал все ненужные данные. Это бы позволило избежать массы ошибок без существенного снижения производительности.

Секретная информация может очутиться еще в нескольких местах. Все данные в конце концов попадают в регистры процессора. Большинство язы­ ков программирования не имеют средств для очистки регистров. Впрочем,

9.3 Как сохранить секреты

161

в процессорах наподобие Pentium, которые страдают от извечного “дефицита” регистров, вероятность того, что данные продержатся в регистрах на протя­ жении хоть сколько-нибудь долгого времени, крайне мала.

Во время переключения контекста (когда операционная система переклю­ чается с одной работающей программы на другую) значения, находящиеся в регистрах процессора, перемещаются в оперативную память, где могут оставаться на протяжении долгого времени. Насколько мы знаем, эта про­ блема не имеет иного решения, кроме внесения исправлений в саму операци­ онную систему, что будет гарантировать конфиденциальность этих данных.

9.3.2Файл подкачки

Большинство операционных систем (включая все текущие версии Win­ dows и все версии UNIX) используют принцип виртуальной памяти для уве­ личения числа программ, которые могут быть запущены параллельно. В про­ цессе работы программы не все ее данные хранятся в физической оператив­ ной памяти. Некоторые из них переносятся в файл подкачки, находящийся на жестком диске. Когда программа пытается осуществить доступ к данным, которых нет в оперативной памяти, ее выполнение прерывается. Система вир­ туальной памяти извлекает необходимые данные из файла подкачки и пере­ носит их обратно в оперативную память, после чего выполнение программы может быть продолжено. Если же системе виртуальной памяти понадобится больше свободной памяти, она извлекает из оперативной памяти произволь­ ный сегмент данных и переносит его в файл подкачки.

Разумеется, большинство систем виртуальной памяти не предпринимают каких-либо серьезных попыток, чтобы сохранить данные в секрете или за­ шифровать их перед перенесением на диск. Практически все программные продукты ориентированы на коллективную работу единомышленников, а не на противоборствующее окружение, с которым имеют дело криптографы. Проблема состоит в том, что система виртуальной памяти может взять ка­ кие-нибудь данные нашей программы и записать их на жесткий диск в файл подкачки. Программа об этом даже и не узнает. Предположим, что в файл подкачки были перенесены секретные ключи. Если компьютер даст сбой или неожиданно будет выключен, данные останутся на диске. Следует отметить, что большинство операционных систем оставляют данные на диске и при корректном выключении компьютера. Механизма очищения файла подкач­ ки, как правило, не существует, поэтому данные могут оставаться на диске до бесконечности. А что, если файл подкачки когда-нибудь попадет в руки