ua
ua
ГоловнаБлог

Поддержка 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);
//...

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

P.S. Чтобы получать наши новые статьи раньше других или просто не пропустить новые публикации — подписывайтесь на нас в FacebookVKTwitterLiveJournal и LinkedIn

Сергій Харланчук
Senior Web Developer / Team Lead, SECL Group

Схожі публікації

Більше про нас?
Компанія
Більше кейсів?
Роботи
Є проект?
Контакти
Канада

240 Richmond Street W
Toronto ON M5V 1V6
+1 (647) 946-92-12

США

3524 Silverside Road
35B, Wilmington,
Delaware 19810-4929
+1 (929) 237-12-11

Україна

79039, м. Львів,
вул. Дмитра Бортнянського, 23
+380 (44) 389-90-39

Copyright © 2005 – 
2024
, ГК «SECL Group»