RabbitMQ & PHP & OpenSSL

В прошлом посте я рассказывал про замечательный облачный сервис RabbitMQ as a Service.

Тем кто будет работать с rabbitmq из PHP, стоит детально изучить мануал с сайта (перевод на русский, только код немного устарел), единственная проблема не учтенная в мануале — это безопасность.

Если мы передаем данные через открытый канал связи, или пользуемся публичными ресурсами, а в нашем случае это так, тот кто владеет сервисом или потенциально получит доступ к учетной записи службы — сможет управлять всем тем, что вы накрутили на данном сервисе, но уже на нашей стороне.

Как нам защититься?

Решать проблему безопасности, мы будем путем добавления электронной подписи в сообщение при помощи openssl.
Дальше пойдет немного «индусского», но вполне рабочего кода.

1. Создаем закрытый ключ и сертификат:

openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout privateKey.key -out certificate.crt \
  -subj '/C=RU/ST=Moscow/L=Moscow/O=IT/OU=IT Departament/CN=test.ru/emailAddress=test@test.ru'
openssl pkcs12 -export -out  cert.p12 -in certificate.crt -inkey privateKey.key

На выходе получим 3 файла:
certificate.crt — Публичный сертификат;
privateKey.key — Приватный ключ;
cert.p12 — Файл, содержащий ключевую пару (закрытый ключ и сертификат).

2. Для работы с rabbitmq нам понадобиться composer расширение php-amqplib/php-amqplib:

composer.json

{
  "require": {
    "php-amqplib/php-amqplib": ">=2.6.1"
  }
}

3. Код сендера сообщений:

sender.php

<?php
require('vendor/autoload.php');
#define('AMQP_DEBUG', true);
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
 
//Подключаемся к rabbitmq
$url = parse_url('amqp://cecmqwhe:PASSWORD@puma.rmq.cloudamqp.com/cecmqwhe');
$connection = new AMQPStreamConnection($url['host'], 5672, $url['user'], $url['pass'], substr($url['path'], 1));
$channel = $connection->channel();
 
$channel->queue_declare('hello', false, false, false, false);
 
 
if (!$data = file_get_contents("cert.p12")) {
    echo "Error: Unable to read the cert file\n";
    exit;
}
 
//Открываю файл с ключевой парой и извлекаю сертификат и закрытый ключ. "123456" пароль указанный при конвертации файла cert.p12
openssl_pkcs12_read($data, $container,'123456');
$privatekey = openssl_get_privatekey($container['pkey']);
$pubkey = openssl_pkey_get_public($container['cert']);
 
 
$str='test';
 
//подписываю текст
openssl_sign($str, $signature, $privatekey);
 
// Освобождаем память от закрытого ключа
openssl_free_key($privatekey);
 
// Мы будем передавать в rabbit сериализованный массив (т.е. представленное в виде строки)
// Так как подпись содержит бинарные данные, мы также её преобразуем через base64_encode
$text = serialize(array('str'=>$str,'signature'=>base64_encode($signature)));
 
echo $text;
 
$msg = new AMQPMessage($text);
$channel->basic_publish($msg, '', 'hello');
echo " [x] Sent 'Hello World!'\n";
$channel->close();
$connection->close();
?>

4. Код ресивера:

receive.php

<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
 
$url = parse_url('amqp://cecmqwhe:PASSWORD@puma.rmq.cloudamqp.com/cecmqwhe');
$connection = new AMQPStreamConnection($url['host'], 5672, $url['user'], $url['pass'], substr($url['path'], 1));
 
// Для работы нам не нужна ключевая пара, только открытый сертификат:
 
if (!$data = file_get_contents("certificate.crt")) {
    echo "Error: Unable to read the cert file\n";
    exit;
}
 
$cert=file_get_contents('certificate.crt');
$pubkey = openssl_pkey_get_public($cert);
 
$channel = $connection->channel();
$channel->queue_declare('hello', false, false, false, false);
echo ' [*] Waiting for messages. To exit press CTRL+C', "\n";
 
$callback = function($msg) {
 
    global $pubkey;
    // Проводим десериализацию 
    $data = unserialize($msg->body);
    echo " [x] Received ", $data['str'], "\n";
 
    //проверяю ЭЦП
    $ok = openssl_verify($data['str'],base64_decode($data['signature']), $pubkey);
 
    if ($ok == 1) {
        echo "ЭЦП корректна :)";
    } else {
        echo "ЭЦП не корректна!";
    }
};
 
$channel->basic_consume('hello', '', false, true, false, false, $callback);
while(count($channel->callbacks)) {
    $channel->wait();
}
$channel->close();
$connection->close();
?>

5. Проверка кода:

# php sender.php
a:2:{s:3:"str";s:4:"test";s:9:"signature";s:344:"Zi7NGKOU....<cut>==";} 
[x] Sent 'Hello World!'
 
# php receive.php
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received test
ЭЦП корректна :)

Код далеко не идеален, и не рассматривает большинство угроз, которые могут возникнуть в ходе эксплуатации, но по крайней мере позволит однозначно защитить сообщение от подделки.

Вы можете оставить комментарий ниже.