Все опять началось с любопытства) Мне стало интересно, как работает изнутри протокол openid - хотелось выяснить, есть ли тонкие моменты, которые при необдуманном использовании могут привести к разного рода проблемам. Ну а раз появился интерес, то все остальное отступает на второй план - поэтому начнем!
Основные понятия
Конечный пользователь (End User) - собственно, реальный человек, который использует openid.
Потребитель (Consumer or Relying Party) - сайт, на котором пользователь пытается зарегистрироваться.
Сторона, обеспечивающая аутентификацию (Identity Provider) - хост, который подтверждает пользователя.
Браузер (User Agent)
Взаимодействие заинтересованных сторон
Для начала опишу основные виды взаимодействия:
Основные понятия
Конечный пользователь (End User) - собственно, реальный человек, который использует openid.
Потребитель (Consumer or Relying Party) - сайт, на котором пользователь пытается зарегистрироваться.
Сторона, обеспечивающая аутентификацию (Identity Provider) - хост, который подтверждает пользователя.
Браузер (User Agent)
Взаимодействие заинтересованных сторон
Для начала опишу основные виды взаимодействия:
- Прямая коммуникация - Consumer и Indentity Server взаимодействуют по HTTP напрямую с помощью POST запросов.
- Непрямая коммуникация - Consumer и Identity Server взаимодействуют по HTTP через User Agent с помощью GET запросов.
- "Глупый" (dumb) режим коммуникации - при регистрации пользователя не сохраняется никаких состояний (т.е. при повторном посещении пользователю вновь придется регистрироваться, проходя все этапы)
- "Умный" (smart) режим коммуникации - при регистрации пользователя, он запоминается и в последующем может войти на сайт минуя некоторые этапы аутентификации.
Сообщения, отправляемые в процессе коммуникации
Теперь опишу сам процесс использования openid (сконцентрируюсь на сообщениях, которыми обмениваются consumer, identity server и браузер). Итак, что происходит, когда пользователь жмет на кнопку "Войти":
Теперь опишу сам процесс использования openid (сконцентрируюсь на сообщениях, которыми обмениваются consumer, identity server и браузер). Итак, что происходит, когда пользователь жмет на кнопку "Войти":
- Associate Message - сообщение, которое consumer отсылает identity server'у. Оно предназначено для установления общего секретного ключа с помощью алгоритма Диффи - Хеллмана, который затем используется для подписи последующих сообщений. Если используется smart режим, то данный этап может быть пропущен.
Чтобы самому посмотреть, что представляет из себя это сообщение, можно вооружиться wireshark'ом, локально запустить какой-нибудь простеньких сайт, позволяющий зарегиться на нем с помощью openid, и снять дам регистрации. Затем откроем дамп, в качестве фильтра укажем пакеты, относящиеся к http.
- Затем выберем интересующий запрос и в контекстном меню выберем пункт "Folow TCP stream" и увидим примерно следующее (в качестве identity server использовался openid.yandex.ru):
OST http://openid.yandex.ru/server/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded; charset=utf-8 User-Agent: DotNetOpenAuth/3.4.3.10103 Host: openid.yandex.ru Cache-Control: no-store,no-cache Pragma: no-cache Content-Length: 569 Expect: 100-continue Proxy-Connection: Close openid.dh_modulus=ANz5OguIOXLsDhmYmsWizjEOHTdxfo2Vcbt2I3MYZuYe91ouJ4mLBX%2BYkcLiemOcPym2CBRYHNOyyjmG0mg3BVd9RcLn5S3IHHoXGHblzqdLFEi%2F368Ygo79JRnxTkXjgmY0rxlJ5bU1zIKaSDuKdiI%2BXUkKJX8Fvf8W8vsixYOr&openid.dh_gen=Ag%3D%3D&openid.dh_consumer_public=AJs12O5ypo2N%2FL0RJiiOgu9llg2dFsnjthyH49dx6FXz52iDXNkS7gquOm6KEr%2BUfTmktyVMA5DrZwJ%2BrX1jk7sKmXJMmi9%2B7N5fa0wvz%2Fi6nrvg8Oqw31kh%2BtbD9ansUeATSlCfUoRCqeUHEABv%2BfEoZlgh%2BeU71rlInEppkiuX&openid.assoc_type=HMAC-SHA256&openid.session_type=DH-SHA256&openid.mode=associate&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0
Куча параметров POST запроса не должна смущать, потому что интересных их них всего несколько:
openid.assoc_type - указывает тип алгоритма, который будет использоваться для подписывания (signing) сообщения. В нашем случае это HMAC_SHA256
openid.session_type - указывает тип шифрования, который будет использоваться для MAC (Message Authentification Code). В нашем сообщении это DH-SHA256.
openid.dh_modulus, openid.dh_gen, openid.dh_consumer_public - параметры, которые используются в алгоритме Диффи - Хеллмана для получения секретного ключа. - Ответ Identity Server'а выглядит следующим образом:
HTTP/1.1 200 OK Connection: Keep-Alive Proxy-Connection: Keep-Alive Transfer-Encoding: chunked Date: Mon, 01 Aug 2011 10:39:20 GMT Content-Type: text/html; charset=utf-8 Server: lighttpd / Pony powered 18b assoc_handle:{HMAC-SHA256}{4e368258}{sNK4LA==} assoc_type:HMAC-SHA256 dh_server_public:VL+NAUDd5jZWYas++bZf/oEpW4dYxxlSU0FBHx2D48iN8Fv/uxJMsYHPJUSpPDrqWxSm8NDMKA5MOJcmZVPUfkQHdmnSmP09a9n65FkG+AIKTQHLLBflUVgDtM5Ivu/AhfYeMZFyp7JeEHcgUZ4F4YxjKBGCS+8wdrBWfjrHFOs= enc_mac_key:i9CapoRXcNYv1bKQWSPumj1EfeOVvUYyGzTcnnaFJGA= expires_in:1209600 ns:http://specs.openid.net/auth/2.0 session_type:DH-SHA256
Интересные параметры:
assoc_handle - может быть использован в последующих сообщениях, определяет какие ключи должны быть использованы вновь для шифрования/дешифрования.
expires_in - время, по прошествии которого потребуется новое association message.
dh_server_public - публичный ключ Identity Server'а, который используется в алгоритме Диффи - Хеллмана.
enc_mac_key - зашифрованный Message Authentification Code.
- Check_Setup Message - сообщение, используемое для получения подтверждающей информации (аналогом также является check_immediate, который используется совместно с ajax). Сообщение является непрямым и оправляется consumer'ом identity server'у через браузер пользователя GET запросом, который имеет примерно следующий вид:
GET http://openid.yandex.ru/server/?openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=%7BHMAC-SHA256%7D%7B4e368258%7D%7BsNK4LA%3D%3D%7D&openid.return_to=http%3A%2F%2Fwww.some_site.com%3A8000%2Fauth%2Freceive%3FDomain%3Dwww.some_site.com%26dnoa.userSuppliedIdentifier%3Dhttp%253A%252F%252Fopenid.yandex.ru%252F&openid.realm=http%3A%2F%2F%2A.some_site.com%3A8000%2F&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.ns.sreg=http%3A%2F%2Fopenid.net%2Fextensions%2Fsreg%2F1.1&openid.sreg.required=email%2Cfullname&openid.sreg.optional=&openid.ns.alias3=http%3A%2F%2Fopenid.net%2Fsrv%2Fax%2F1.0&openid.alias3.required=alias1%2Calias2%2Calias3%2Calias4&openid.alias3.mode=fetch_request&openid.alias3.type.alias1=http%3A%2F%2Faxschema.org%2Fcontact%2Femail&openid.alias3.count.alias1=1&openid.alias3.type.alias2=http%3A%2F%2Faxschema.org%2FnamePerson%2Ffirst&openid.alias3.count.alias2=1&openid.alias3.type.alias3=http%3A%2F%2Faxschema.org%2FnamePerson%2Flast&openid.alias3.count.alias3=1&openid.alias3.type.alias4=http%3A%2F%2Faxschema.org%2Fcontact%2Fcity%2Fhome&openid.alias3.count.alias4=1 HTTP/1.1 [...skipped...]
Опять же куча параметров, но уже в строке запроса) Но в целом, можно выделить наиболее интересные:
openid.assoc_handle - параметр, полученный от сервера.
openid.return_to - URL, по которому необходимо направить пользователя, после подтверждения.
- Ответ, полученный от сервера на check_setup выглядит примерно следующим образом:
HTTP/1.1 302 Found Connection: Keep-Alive Proxy-Connection: Keep-Alive Content-Length: 0 Date: Mon, 01 Aug 2011 10:39:21 GMT Location: http://www.some_site.com/auth/receive?Domain=www.some_site.com&dnoa.userSuppliedIdentifier=http%3A%2F%2Fopenid.yandex.ru%2F&openid.assoc_handle=%7BHMAC-SHA256%7D%7B4e368258%7D%7BsNK4LA%3D%3D%7D&openid.ax.count.alias1=1&openid.ax.count.alias2=0&openid.ax.count.alias3=0&openid.ax.count.alias4=0&openid.ax.mode=fetch_response&openid.ax.type.alias1=http%3A%2F%2Faxschema.org%2Fcontact%2Femail&openid.ax.type.alias2=http%3A%2F%2Faxschema.org%2FnamePerson%2Ffirst&openid.ax.type.alias3=http%3A%2F%2Faxschema.org%2FnamePerson%2Flast&openid.ax.type.alias4=http%3A%2F%2Faxschema.org%2Fcontact%2Fcity%2Fhome&openid.ax.value.alias1.1=some_user%40yandex.ru&openid.claimed_id=http%3A%2F%2Fopenid.yandex.ru%2Fsome_user%2F&openid.identity=http%3A%2F%2Fopenid.yandex.ru%2Fsome_user%2F&openid.mode=id_res&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.ns.ax=http%3A%2F%2Fopenid.net%2Fsrv%2Fax%2F1.0&openid.ns.sreg=http%3A%2F%2Fopenid.net%2Fextensions%2Fsreg%2F1.1&openid.op_endpoint=http%3A%2F%2Fopenid.yandex.ru%2Fserver%2F&openid.response_nonce=2011-08-01T10%3A39%3A21ZVnmyMZ&openid.return_to=http%3A%2F%2Fwww.some_site.com%3A8000%2Fauth%2Freceive%3FDomain%3Dwww.some_site.com%26dnoa.userSuppliedIdentifier%3Dhttp%253A%252F%252Fopenid.yandex.ru%252F&openid.sig=BfsP%2FvUND29UXy1W7heEMV5oVH8YvOUzS8SbT12fQK0%3D&openid.signed=assoc_handle%2Cax.count.alias1%2Cax.count.alias2%2Cax.count.alias3%2Cax.count.alias4%2Cax.mode%2Cax.type.alias1%2Cax.type.alias2%2Cax.type.alias3%2Cax.type.alias4%2Cax.value.alias1.1%2Cclaimed_id%2Cidentity%2Cmode%2Cns%2Cns.ax%2Cns.sreg%2Cop_endpoint%2Cresponse_nonce%2Creturn_to%2Csigned%2Csreg.email%2Csreg.fullname&openid.sreg.email=some_user%40yandex.ru&openid.sreg.fullname=some_name
Вновь нечто трудночитаемое) Кстати, для большего удобства исследования подобных запросов могу посоветовать Burp Suite, но это относится только к непрямым запросам, которые проходят через браузер.
Здесь уже больше важных параметров, которые стоит рассмотреть:
openid.assoc_handle - все тот же параметр, что и раньше. Consumer будет использовать его для верификации.
openid.responce_nonce - параметр, используемый для создания уникального сообщения. Состоит из времени сервера в формате UTC и некоторых дополнительных символов, определяемых сервером.
openid.signed - список параметров, разделенных запятой, которые подписаны (signed).
openid.sig - сигнатура сообщения.
Также сообщение содержит всю основную информацию о пользователе - идентификатор, email, имя и проч.
- Check_authentification - проверочное сообщение. Не отсылается, если ассоциация между consumer'ом и identity server'ом уже существует.
Верификация сообщений
Теперь немного о смысле тех параметров, которые непосредственно относятся к верификации переданных сообщений. Идея состоит в следующем - с помощью association message и алгоритма Диффи - Хеллмана consumer и identity server получают общий ключ (shared secret), который вместе с assoc_handle используется в дальнейшем для подписывания информации о пользователе, которая передается в последующих сообщениях.
Вот как это выглядит:
Consumer и identity server используют не зашифрованные параметры p и g. По умолчанию, g=2, а base64(btwoc(p))=
Затем каждый из них выбирает произвольные числа x и y и получают публичные ключи - X=g^x mod p , Y=g^y mod p. Именно ими они обмениваются в association message. После получения публичных ключей, проделывалась следующая операция : K1=Y^x mod p , K2=X^y mod p. Можно заметить, что K1=Y^x mod p = (g^y mod p)^x mod p = (g^x mod p)^y mod p = K2. K=K1=K2 и является секретным ключом, который передается между сторонами.
Теперь посмотрим, как получается MAC. Заглянув в спецификацию, увидим, что enc_mac_key формируется следующим образом - base64(H(btwoc(g ^ (xa * xb) mod p)) XOR MAC key), где H - это хэширование SHAR256. Consumer, зная общий ключ (K=K1=K2), может получить и хэш Н(btwoc(g ^ (xa * xb) mod p)), а затем и сам MAC.
Информация о пользователе, подписывается с помощью MAC - сигнатуру подписи мы видим в соответствующем параметре. Openid.sig формируется следующим образом base64(HMAC( MAC, token_contents )), где token_content содержит параметры, которые подлежат подписи (openid.signed), а о HMAC можно почитать здесь.
Мои замечания
Здесь опишу свои идеи - просто чтобы не забыть. Итак...
На мой взгляд, самый простой способ обхода такой аутентификации - подмена параметров, который проходят через браузер пользователя, и несут информацию о нем. Т.о. можно аутентифицироваться привилегированным пользователем, зная всего лишь его email. Но для этого надо догадаться, как обходить подпись параметров сообщения. Предполагая, что MAC мы не узнаем, т.к. не имеем возможность для атаки MITM, то остается возможность обнулить параметр openid.signed, который как раз и говорит, что именно нужно подписывать, а затем соответствующим образом изменить сигнатуру - но, к сожалению, видимо, HMAC от пустой строки возвращает не пустой результат, поскольку аутентифицироваться с пустыми полями openid.sig и openid.signed не получается. В этом надо будет разобраться)
Еще интересный вопрос - можно ли, отправить consumer'у фальшивый ответ, содержащий публичный ключ, вычисления по которому секретного ключа даст заранее известный результат?
Одни вопросы)
DCF93A0B883972EC0E19989AC5A2CE310E1D37717E8D9571BB7623731866E61E
F75A2E27898B057F9891C2E27A639C3F29B60814581CD3B2CA3986D268370557
7D45C2E7E52DC81C7A171876E5CEA74B1448BFDFAF18828EFD2519F14E45E382
6634AF1949E5B535CC829A483B8A76223E5D490A257F05BDFF16F2FB22C583AB
btwoc - big endian two complete - функция, которая принимает целое и возвращает его наименьшее дополнение до двух (big-endian - в порядке от младшего к старшему).Затем каждый из них выбирает произвольные числа x и y и получают публичные ключи - X=g^x mod p , Y=g^y mod p. Именно ими они обмениваются в association message. После получения публичных ключей, проделывалась следующая операция : K1=Y^x mod p , K2=X^y mod p. Можно заметить, что K1=Y^x mod p = (g^y mod p)^x mod p = (g^x mod p)^y mod p = K2. K=K1=K2 и является секретным ключом, который передается между сторонами.
Теперь посмотрим, как получается MAC. Заглянув в спецификацию, увидим, что enc_mac_key формируется следующим образом - base64(H(btwoc(g ^ (xa * xb) mod p)) XOR MAC key), где H - это хэширование SHAR256. Consumer, зная общий ключ (K=K1=K2), может получить и хэш Н(btwoc(g ^ (xa * xb) mod p)), а затем и сам MAC.
Информация о пользователе, подписывается с помощью MAC - сигнатуру подписи мы видим в соответствующем параметре. Openid.sig формируется следующим образом base64(HMAC( MAC, token_contents )), где token_content содержит параметры, которые подлежат подписи (openid.signed), а о HMAC можно почитать здесь.
Мои замечания
Здесь опишу свои идеи - просто чтобы не забыть. Итак...
На мой взгляд, самый простой способ обхода такой аутентификации - подмена параметров, который проходят через браузер пользователя, и несут информацию о нем. Т.о. можно аутентифицироваться привилегированным пользователем, зная всего лишь его email. Но для этого надо догадаться, как обходить подпись параметров сообщения. Предполагая, что MAC мы не узнаем, т.к. не имеем возможность для атаки MITM, то остается возможность обнулить параметр openid.signed, который как раз и говорит, что именно нужно подписывать, а затем соответствующим образом изменить сигнатуру - но, к сожалению, видимо, HMAC от пустой строки возвращает не пустой результат, поскольку аутентифицироваться с пустыми полями openid.sig и openid.signed не получается. В этом надо будет разобраться)
Еще интересный вопрос - можно ли, отправить consumer'у фальшивый ответ, содержащий публичный ключ, вычисления по которому секретного ключа даст заранее известный результат?
Одни вопросы)



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