div.main {margin-left: 20pt; margin-right: 20pt}Практика peer-to-peer
приложений: P2P в сочетании с SSL
Безопасность соединений между партнерами.
Основным требованием для любых не тривиальных P2P приложений
является защита обмена данными между партнерами. Детали защиты зависят от того,
как приложение будет использоваться и что именно необходимо защитить. Часто
можно реализовать мощную защиту используя готовые технологии, такие как SSL. В
этой статье Тодд Сандстед (Todd Sundsted) демонстрирует как можно использовать
SSL (через JSSE) для защиты P2P.
Уровень доверия оценивает степень конфиденциальности по отношению к тому, с кем
мы обмениваемся данными и доступ к ресурсам, которые мы предоставляем. Также
были рассмотрены три функциональных блока, используемых для достижения доверия
во всех распределенных приложениях, включая P2P приложения: идентификация,
авторизация и шифрование.
Сейчас мы используем накопленный опыт для того, чтобы
модифицировать наше простенькое P2P приложение. Мы расширим приложение таким
образом, чтобы оно поддерживало P2P идентификацию и шифрование с использованием
сертификатов X.509. Авторизация будет рассмотрена в следующей статье.
Защита идентификации
Требования к защите приложений в основном зависят от того, как
будет использоваться приложение и что необходимо защитить. Однако, часто можно
реализовать мощную защиту используя стандартные, "готовые" технологии.
Прекрасным примером этого служит идентификация.
Когда клиент пытается купить товар через Web сайт, и клиент и Web
сайт выполняют идентификацию. Покупатель как правило идентифицирует себя,
указывая имя и пароль. С другой стороны, Web сайт идентифицирует себя,
обмениваясь блоками подписанных данных и соответствующим сертификатом X.509,
являющимся частью процесса "рукопожатия" (handshake) SSL. Броузер клиента
выполняет проверку сертификата и использует прилагающийся публичный ключ для
проверки подписанных данных. После завершении аутентификации обеих сторон сделка
может быть совершена.
SSL может использоваться как при аутентификации сервера (см.
приведенный выше пример), так и при аутентификации клиента, используя схожий
механизм. Как правило Web серверы не используют SSL для аутентификации клиентов,
поскольку проще выполнить проверку пароля. Однако предпочтительней использовать
прозрачную аутентификацию клиента и сервера, производимую между партнерами, при
помощи SSL, такую, которая используется в P2P приложениях.
Secure Sockets Layer (SSL)
SSL - это безопасный протокол, обеспечивающий приватность при
взаимодействиях через такие сети как Internet. SSL позволяет приложениям
взаимодействовать не опасаясь подглядывания и постороннего вмешательства.
По сути SSL - это два протокола, которые работают вместе: протокол
записи (SSL Record Protocol) и протокол для соединения (SSL Handshake Protocol).
Протокол записи - нижний уровень двух протоколов, шифрующий и
дешифрующий записи данных различной длины и используемый протоколами более
высокого уровня, такими как протокол для соединения. Протокол соединения
осуществляет обмен и проверку удостоверений приложений.
Когда приложение (клиент) хочет взаимодействовать с другим
приложением (сервером), клиент открывает сокетное (socket) соединение с
сервером. Клиент и сервер устанавливают защищенное соединение. В процессе этого
взаимодействия, сервер идентифицирует себя для клиента. При необходимости клиент
может также идентифицировать себя для сервера. Когда идентификация завершена и
установлено безопасное соединение, оба приложения могут безопасно
взаимодействовать между собой.
Условно я называю партнера, инициирующего соединение, клиентом, а
другого - сервером, вне зависимости от того какую роль они выполняют в
дальнейшем, после установления соединения.
Как использовать SSL в Java приложениях
SSL для Java приложений обеспечивается через Java Secure Socket
Extension (JSSE). JSSE в ходит в стандартный комплект поставки JDK 1.4, а для
более ранних версий платформы Java она доступна в качестве дополнения.
JSSE использует механизм SSL для своих защищенных socket'ов.
Socket'ы JSSE работают как обычные socket'ы, отличие состоит в том, что они
поддерживают прозрачную идентификацию и шифровку. Поскольку они выглядят как
обыкновенные socket'ы (они являются подклассами java.net.Socket и
java.net.ServerSocket), большая часть кода нашего приложения при использовании
JSSE не нуждается в изменениях. Наиболее сильно видоизменится лишь код,
отвечающий за создание и инициализацию фабрик (factory) защищенных
socket'ов.
Если вы хотите использовать JSSE на версиях платформы Java ранее
1.4, вам необходимо самостоятельно скачать и инсталлировать дополнение JSSE
Инструкции по установке настолько просты, что я не считаю нужным приводить их
здесь.
Модель
Рисунок 1 иллюстрирует роль, которую SSL играет при взаимодействии
между партнерами при P2P соединениях.
Рис. 1. SSL в действии
Два партнера, обозначенные как A и B заинтересованы в
безопасном взаимодействии. В контексте нашего простого P2P приложения, партнер A
хочет запросить ресурс у партнера B.
Каждый партнер имеет базу данных (называемую хранилищем
ключей, keystore), которая содержит его приватный ключ и сертификат, содержащий
его публичный ключ. База данных защищена паролем. База данных также содержит
один или более подписанных сертификатов других партнеров, пользующихся
доверием.
Партнер A инициирует транзакцию, каждый партнер
идентифицирует другого, оба партнера договариваются о шифрах и их количестве и
устанавливают безопасный канал. После того как эти операции завершены, каждый
партнер уверен что канал защищен и от кого он получает информацию.
Инициализация
Поскольку при использовании JSSE и SSL наиболее значительно
изменится инициализирующий код, давайте рассмотрим код партнера A, отвечающий за
инициализацию.
Листинг 1. Инициализирующий код системы безопасности // Каждый партнер имеет свой идентификатор, который должен быть локально (не глобально)
// уникальным. Этот идентификатор и связанные с ним публичный и приватный ключи
// хранятся в хранилище ключей (KeyStore) и защищены паролем. Каждый партнер также имеет
// имя, которое должно быть глобально уникальным.
String stringIdentity = null;
String stringPassword = null;
String stringName = null;
// Код, который выполняет идентификацию пользователя и запрашивает его/ее пароль.
// Имя пользователя генерируется (при необходимости) впоследствии.
// Создается домашний каталог. Это кратчайший способ создания домашнего каталога,
// но он связан с некоторыми проблемами --
// различные версии Microsoft Windows помещают каталог в совершенно различные
// места иерархии каталогов.
String stringHome = System.getProperty("user.home") + File.separator + "p2p";
File fileHome = new File(stringHome);
if (fileHome.exists() == false)
fileHome.mkdirs();
// Создается хранилище ключей. Для создания хранилища ключей вы должны запустить внешний процесс,
// поскольку API безопасности не предоставляет такой возможности внутри системы.
// Я недостаточно тщательно тестировал этот фрагмент, чтобы утверждать насколько предлагаемый код
// компактен, но по крайней мере он безотказно работал, где бы я его не использовал.
String stringKeyStore = stringHome + File.separator + "keystore";
File fileKeyStore = new File(stringKeyStore);
if (fileKeyStore.exists() == false)
{
System.out.println("Creating keystore...");
byte [] arb = new byte [16];
SecureRandom securerandom = SecureRandom.getInstance("SHA1PRNG");
securerandom.nextBytes(arb);
stringName = new String(Base64.encode(arb));
String [] arstringCommand = new String []
{
System.getProperty("java.home") + File.separator + "bin" + File.separator + "keytool",
"-genkey",
"-alias", stringIdentity,
"-keyalg", "RSA",
"-keysize", "1024",
"-dname", "CN=" + stringName,
"-keystore", stringHome + File.separator + "keystore",
"-keypass", stringPassword,
"-storetype", "JCEKS",
"-storepass", stringPassword
};
Process process = Runtime.getRuntime().exec(arstringCommand);
process.waitFor();
InputStream inputstream2 = process.getInputStream();
IOUtils.copy(inputstream2, System.out);
InputStream inputstream3 = process.getErrorStream();
IOUtils.copy(inputstream3, System.out);
if (process.exitValue() != 0)
System.exit(-1);
}
// Как только приложение создает/находит хранилище ключей, оно
// открывает его и создает на базе хранящихся там данных свой
// экземпляр KeyStore.
char [] archPassword = stringPassword.toCharArray();
FileInputStream fileinputstream = new FileInputStream(stringHome + File.separator +
"keystore");
KeyStore keystore = KeyStore.getInstance("JCEKS");
try
{
keystore.load(fileinputstream, archPassword);
}
catch (IOException ioexception)
{
System.out.println("Cannot load keystore. Password may be wrong.");
System.exit(-3);
}
if (keystore.containsAlias(stringIdentity) == false)
{
System.out.println("Cannot locate identity.");
System.exit(-2);
}
// Создание менеджеа ключей. Менеджер ключей хранит приватный ключ
// этого партнера.
KeyManagerFactory keymanagerfactory = KeyManagerFactory.getInstance("SunX509");
keymanagerfactory.init(keystore, archPassword);
KeyManager [] arkeymanager = keymanagerfactory.getKeyManagers();
// Создание менеджера доверия. Менеджер дверия хранит сертификаты
// других партнеров.
TrustManagerFactory trustmanagerfactory = TrustManagerFactory.getInstance("SunX509");
trustmanagerfactory.init(keystore);
TrustManager [] artrustmanager = trustmanagerfactory.getTrustManagers();
// Создание SSL контекста.
SSLContext sslcontext = SSLContext.getInstance("SSL");
SecureRandom securerandom = SecureRandom.getInstance("SHA1PRNG");
sslcontext.init(arkeymanager, artrustmanager, securerandom);
// Создание фабрик.
m_socketfactory = sslcontext.getSocketFactory();
m_serversocketfactory = sslcontext.getServerSocketFactory();
m_keystore = keystore;
Когда пользователь в первый раз запускает приложение, приложение
предлагает ему ввести идентификатор (имя) и пароль. Идентификатор используется
лишь для того, чтобы локально идентифицировать партнеров - он не имеет
глобального значения. Приложение генерирует случайную 128-битную (16-байтную)
строку, которая используется для глобальной идентификации партнеров и
трансформирует ее в буквенно-числовую строку. Она использует идентификатор,
пароль и имя для создания хранилища ключей и пары публичных/приватных ключей,
которые она записывает в хранилище ключей. Пароль защищает информацию в
хранилище ключей.
Я уверен что вы обратили внимание на подход, который я использовал
для создания инициирующего хранилища ключей. Приложение запускает утилиту
keytool как внешний процесс и она создает хранилище ключей. Я использовал такой
подход, поскольку публичные API безопасности Java не предоставляют инструментов
для создания сертификатов - эта возможность скрыта в JSSE и его API не
публикуются. Наибольший недостаток связанный с запуском keytool связан с тем,
что указываемый пользователем пароль, передаваемый в качестве одного из
параметров, может быть перехвачен.
После того как приложение создаст хранилище ключей, оно открывает
его и загружает в память (если бы мы могли создавать хранилище ключей
непосредственно, этот шаг был бы излишним). Затем из хранилища ключей создается
менеджер ключей и менеджер доверия. Менеджер ключей управляет
ключами, используемыми для идентификации приложения его партнером при работе
через защищенный socket. Менеджер доверия управляет сертификатами, которые
используются для идентификации партнера, сидящего на другом конце защищенного
socket'а.
Наконец, приложение создает экземпляр SSLContext, который
выступает в роли фабрики для фабрик защищенных socket'ов. Оно создает защищенные
версии классов SocketFactory и ServerSocketFactory и использует их
при дальнейшем взаимодействии.
Тодд Сандстед (Todd Sundsted)
|