...

среда, 15 января 2014 г.

Поддержка sha512 в wsse-authentication-bundle от Escape Studios, Symfony2

Недавно встала задача повышения безопасности при создании токена, а также поддержки sha512. Статья получилась узконаправленная, но я уверен, что сталкиваюсь с подобным не только я.

Для решения текущих задач при программировании API интернет-магазина на Symfony2 решил подружить FOSUserBundle и WSSEAuthenticationBundle c алгоритмом sha512 и вскоре выяснил, что для этого потребуется небольшая доработка. Об этом и пойдет речь в моей статье.



Базовые настройки:




app/config/config.yml

fos_user:

db_driver: orm

firewall_name: wsse_secured

user_class: Acme\DemoBundle\Entity\User

# Escape WSSE authentication configuration

escape_wsse_authentication:

authentication_provider_class: Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Provider

authentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listener

authentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPoint

authentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder


app/config/security.yml

security:

providers:

fos_userbundle:

id: fos_user.user_provider.username

encoders:

FOS\UserBundle\Model\UserInterface: sha512


firewalls:

wsse_secured:

pattern: ^/api/.*

wsse:

lifetime: 300 #lifetime of nonce

realm: "Secured API" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate)

profile: "UsernameToken" #WSSE profile (WWW-Authenticate)

encoder: #digest algorithm

algorithm: sha512

encodeHashAsBase64: true

iterations: 1

anonymous: true


Код генерации токена в контроллере:




src\Acme\DemoBundle\Controller\SecurityController.php

//...

$created = date('c');

$nonce = substr(md5(uniqid('nonce_', true)), 0, 16);

$nonceHigh = base64_encode($nonce);

$salted = $nonce . $created . $user->getPassword() . "{" . $user->getSalt() . "}";

$passwordDigest = hash('sha512', $salted, true);

$header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";

$view->setHeader("Authorization", 'WSSE profile="UsernameToken"');

$view->setHeader("X-WSSE", "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"");

$data = array('WSSE' => $header);

//...

Очень хотелось, чтобы такая конфигурация заработала из коробки, но так не случилось. Разберемся почему. Выяснилось, что в стандартном провайдере от Escapestudios есть такие строки:


WSSEAuthenticationBundle/Security/Core/Authentication/Provider/Provider.php

//...

//validate secret

$expected = $this->encoder->encodePassword(

sprintf(

'%s%s%s',

base64_decode($nonce),

$created,

$secret

),

""

);


Интерес привлекают кавычки в предпоследней строке, если вместо них добавить соль, то все чудесным образом начинает работать. Давайте перепишем этот провайдер в своем бандле и подправим ситуацию:


src\Acme\DemoBundle\Security\Authentication\Provider\WsseProvider.php


namespace Acme\DemoBundle\Security\Authentication\Provider;


use Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Provider;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;

use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;

use Symfony\Component\Security\Core\Exception\NonceExpiredException;


/**

* Class WsseProvider

* @package Acme\DemoBundle\Security\Authentication\Provider

*/

class WsseProvider extends Provider implements AuthenticationProviderInterface

{


/**

* @param $user \Symfony\Component\Security\Core\User\UserInterface

* @param $digest

* @param $nonce

* @param $created

* @param $secret

*

* @return bool

* @throws \Symfony\Component\Security\Core\Exception\CredentialsExpiredException

* @throws \Symfony\Component\Security\Core\Exception\NonceExpiredException

*/

protected function validateDigest($user, $digest, $nonce, $created, $secret)

{

//check whether timestamp is not in the future

if (strtotime($created) > time()) {

throw new CredentialsExpiredException('Future token detected.');

}


//expire timestamp after specified lifetime

if (time() - strtotime($created) > $this->getLifetime()) {

throw new CredentialsExpiredException('Token has expired.');

}


//validate that nonce is unique within specified lifetime

//if it is not, this could be a replay attack

if ($this->getNonceCache()->contains($nonce)) {

throw new NonceExpiredException('Previously used nonce detected.');

}


$this->getNonceCache()->save($nonce, time(), $this->getLifetime());


//validate secret

$expected = $this->getEncoder()->encodePassword(

sprintf(

'%s%s%s',

base64_decode($nonce),

$created,

$secret

),

$user->getSalt()

);


return $digest === $expected;

}

}


Хочу заметить, что в последней, на момент написания статьи, версии бандла отключить использование nonces в конфигурации не представляется возможным, и полученный токен валиден только один раз. Чтобы это изменить строки проверки и добавления nonce можно просто удалить.


Добавим этот класс в настройки:

app/config/config.yml


# Escape WSSE authentication configuration

escape_wsse_authentication:

authentication_provider_class: Acme\DemoBundle\Security\Authentication\Provider\WsseProvider

authentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listener

authentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPoint

authentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder


Теперь давайте немножко улучшим защиту.

В настройках энкодера есть такой параметр iterations:


app/config/security.yml

security:

firewalls:

wsse_secured:

wsse:

encoder: #digest algorithm

iterations: 1


Этот параметр отвечает за количество итераций хэширования при кодировании/декодировании токена. По умолчанию он равен «1». Для сравнения, при хэшировании пароля в Symfony2 он составляет «5000» (Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder). Для реализации подобного функционала внесем некоторые изменения в контроллер и конфигурацию:


app/config/security.yml

parameters:

wsse_iterations: 300

security:

firewalls:

wsse_secured:

wsse:

encoder: #digest algorithm

iterations: %wsse_iterations%


src\Acme\DemoBundle\Controller\SecurityController.php

//...

$created = date('c');

$nonce = substr(md5(uniqid('nonce_', true)), 0, 16);

$nonceHigh = base64_encode($nonce);

$container = $this->get('service_container');

$iterations = $container->getParameter('wsse_iterations');

$salted = $nonce . $created . $user->getPassword() . "{" . $user->getSalt() . "}";

$passwordDigest = hash('sha512', $salted, true);

for ($i = 1; $i < $iterations; $i++) {

$passwordDigest = hash('sha512', $passwordDigest . $salted, true);

}

$passwordDigest = base64_encode($passwordDigest);

$header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";

$view->setHeader("Authorization", 'WSSE profile="UsernameToken"');

$view->setHeader(

"X-WSSE",

"UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\""

);

$data = array('WSSE' => $header);

//...


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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


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

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