четверг, 16 сентября 2010 г.

Пишем Jabber-клиент на Delphi. Часть 3

RFC 2831 использование MD5-Digest аутенфикации в SASL

Итак, аутенфикация решает следующие задачи: Передача пароля на сервер, в закрытом виде, защиту от повторяющихся атак (monitoring nc value), защиту (monitoring nonce) в определённый промежуток времени от определённого клиента.  Для того, что бы понять, как работает данный стандарт, разберем основы SASL.

Общие принципы работы SASL

Метод SASL (Simple Authentication and Security Layer) используется для  добавления поддержки аутентификации в различные протоколы соединения. Для аутентификации могут быть использованы различные механизмы.
Имя требуемого механизма задаётся клиентом в команде аутентификации. Если сервер поддерживает указанный механизм, он посылает клиенту последовательность окликов (challenges), на которые клиент посылает ответы (responses), чтобы удостоверить себя. Содержимое окликов и ответов определяется используемым механизмом и может представлять собой двоичные последовательности произвольной длины. Кодировка последовательностей определяется прикладным протоколом. Вместо очередного оклика сервер может послать подтверждение аутентификации или отказ. Кодировка также определяется протоколом. Вместо ответа клиент может послать отказ от аутентификации. Кодировка опять определяется протоколом. В результате обменов откликам и ответами должна произойти аутентификация (или отказ), передача идентификатора клиента (пустой идентификатор влечёт получение идентификатора из аутентификации) серверу и, возможно, договорённость об использовании безопасного транспортного протокола (security layer), включая максимальный размер буфера шифрования.
Идентификатор клиента может отличаться от идентификатора, определяемого из аутентификации, для обеспечения возможности работы прокси.

Реализация на примере механизма MD5-Digest

Схема работы SASL для нашего клиента основана на использовании механизма MD-Digest и имеет следующий алгоритм работы:
Сервер посылает случайную строку nonce, наличие поддержки utf-8 в параметре charset для имени и пароля, алгоритм аутентификации (обязательно md5-sess) в параметре algorithm.
То есть те данные, что мы раскодировали ранее из пакета challenge:
nonce="22647748",qop="auth",charset=utf-8,algorithm=md5-sess

Клиент отвечает строкой, содержащей: идентификатор клиента username, идентификатор домена realm, полученную от сервера случайную строку nonce, случайную строку клиента cnonce, номер запроса (позволяет серверу заметить попытку replay attack) nc. параметр digest-uri (сочетание имени сервиса, имени сервера т.е. 'xmpp/' + JID Domain),  строку responce подтверждающею знание пароля и ответ на оклик (MD5 от имени пользователя, realm, пароля, случайной строки сервера, случайной строки клиента, идентификатора клиента, номера запроса, уровня защиты, digest-uri; некоторые компоненты берутся в виде MD5, некоторые в исходном виде, некоторые в обоих видах), использование utf-8 для имени и пароля, принятый алгоритм шифрования и идентификатор клиента.

То есть, как вы догадались эта та строка, которую мы формируем в ответ:
username="delphi-test",realm="jabber.ru",nonce="22647748",cnonce="2313e069649daa0ca2b76363525059ebd",nc=00000001,qop=auth,digest-uri="xmpp/jabber.ru",charset=utf-8,response=16351f86cc5591312e20b4ccd880eadb

Сервер проверяет ответ на оклик и посылает ответ на ответ в похожем формате (но всё же отличающемся, чтобы клиент мог убедиться в подлинности сервера). Данный механизм слабее системы с открытыми ключами, но лучше простой CRAM-MD5.
Примечание:
Стоит отметить, что может предусматриваться упрощённый протокол повторной аутентификации (начинается сразу с посылки клиентом ответа с увеличенным на 1 номером запроса).

Алгоритм вычисления строки ответа response

Алгоритм вычисления строки ответа response имеет следующую формулу:
response-value  =

         HEX( KD ( HEX(H(A1)),

                 { nonce-value, ":" nc-value, ":",

                   cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))

      A1 = { H( { username-value, ":", realm-value, ":", passwd } ),

           ":", nonce-value, ":", cnonce-value }

      A2 = { "AUTHENTICATE:", digest-uri-value }

Где:
  Выражение { a, b, ... } – означает сложение строк a, b
  HEX(n) - 16-байтовый MD5-хеш n, приведенный в 32 байтовую Hex-строку в нижнем регистре. Фактически строковое представление дайджеста MD5.
  H(s) – 16-байтовый MD5-хеш строки s
  KD(k, s) – объединение данных (строк) k, s
  H({k, ":", s}) - 16-байтовый MD5-хеш, полученный в результате сложения строки k, “:”, S
Как видите, особо ничего сложного нет. Вот алгоритм расчета реализованный мной на Delphi:
Вычисление строки ответа responce


function GenResponse(UserName, realm, digest_uri, Pass, nonce, cnonce : String) : string;
const
  nc         = '00000001';
  gop        = 'auth';
var
  A2, HA1, HA2,
  sJID : String;
  Razdel    : Byte;
  Context   : TMD5Context;
  DigestJID : TMD5Digest;
  DigestHA1 : TMD5Digest;
  DigestHA2 : TMD5Digest;
  DigestResponse : TMD5Digest;
begin
  Razdel := Ord(':');
  // ВЫЧИСЛЯЕМ А1 по формуле RFC 2831
  //  A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
  //           ":", nonce-value, ":", cnonce-value, ":", authzid-value }
  sJID  := format('%S:%S:%S', [username, realm, Pass]);
  MD5Init(Context);
  MD5UpdateBuffer(Context, PByteArray(@sJID[1])  , Length(sJID));
  MD5Final(DigestJID, Context);

  MD5Init(Context);
  MD5UpdateBuffer(Context, PByteArray(@DigestJID),SizeOf(TMD5Digest));
  MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
  MD5UpdateBuffer(Context, PByteArray(@nonce[1])  , Length(nonce));
  MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
  MD5UpdateBuffer(Context, PByteArray(@cnonce[1])  , Length(cnonce));
  MD5Final(DigestHA1, Context);

  // ВЫЧИСЛЯЕМ А2 по формуле RFC 2831
  //  A2       = { "AUTHENTICATE:", digest-uri-value }
  A2   := format('AUTHENTICATE:%S', [digest_uri]);
  MD5Init(Context);
  MD5UpdateBuffer(Context, PByteArray(@A2[1])  , Length(A2));
  MD5Final(DigestHA2, Context);

  // ВЫЧИСЛЯЕМ RESPONSE по формуле RFC 2831
  //  HEX( KD ( HEX(H(A1)),
  //                 { nonce-value, ":" nc-value, ":",
  //                   cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))
  HA1 := LowerCase( PacketToHex(@DigestHA1, SizeOf(TMD5Digest)));
  HA2 := LowerCase( PacketToHex(@DigestHA2, SizeOf(TMD5Digest)));
  MD5Init(Context);
  MD5UpdateBuffer(Context, PByteArray(@HA1[1]),Length(HA1));
  MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
  MD5UpdateBuffer(Context, PByteArray(@nonce[1])  , Length(nonce));
  MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
  MD5UpdateBuffer(Context, PByteArray(@nc[1])  , Length(nc));
  MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
  MD5UpdateBuffer(Context, PByteArray(@cnonce[1])  , Length(cnonce));
  MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
  MD5UpdateBuffer(Context, PByteArray(@gop[1])  , Length(gop));
  MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel));
  MD5UpdateBuffer(Context, PByteArray(@HA2[1]),Length(HA2));
  MD5Final(DigestResponse, Context);
  Result := LowerCase( PacketToHex(@DigestResponse, SizeOf(TMD5Digest)) )
 end;

На входе функция получает параметры рассмотренные нами ранее.

Комментариев нет:

Отправить комментарий