Реклама
Реклама
Реклама
Реклама. Zarub Zarub - зарубежные сервисы, оплата в рублях

Bitrix D7 Security\Sign: Signer, TimeSigner и типичные ошибки

Нужно отдать клиенту непрозрачный параметр — id файла в ссылке, состояние в cookie, код в hidden-поле — и при возврате понять, не подменили ли строку. Шифровать ради этого весь payload часто избыточно: достаточно HMAC-подписи. В D7 для этого есть Bitrix\Main\Security\Sign\Signer и вариант со сроком жизни — TimeSigner.

Рядом по теме: Config\Option и Configuration (откуда берётся ключ по умолчанию), Result и Error, Web\Uri, Application, Context, Request.

Если нужна помощь с модулем или разбором токенов на проде — услуги и контакты на karoche.ru.

Зачем нужна подпись, если это не шифрование?

Подпись отвечает на вопрос «строку меняли после выдачи?». Содержимое при этом остаётся читаемым: в URL вида id=42.абц... число 42 видно любому. Это не AES и не «спрятать секрет от пользователя».

Берите Signer, когда:

  1. Параметр уходит наружу (query, cookie, post) и должен вернуться без правок.
  2. Секрет (ключ) остаётся на сервере; в токене его быть не должно.
  3. Для временных ссылок нужен явный TTL — тогда TimeSigner.

Не берите Signer как замену авторизации сессии или ACL: подпись id=5 не даёт права читать чужой заказ, если вы сами не проверите владельца после unsign.

Минимальный Signer::sign и unsign

sign принимает строку и опциональную соль, возвращает {message}{separator}{signature} (разделитель по умолчанию — точка). unsign проверяет подпись и возвращает исходное сообщение либо кидает \Bitrix\Main\Security\Sign\BadSignatureException.

<?php
use Bitrix\Main\Security\Sign\Signer;
use Bitrix\Main\Security\Sign\BadSignatureException;

$signer = new Signer();
$signed = $signer->sign('42', 'file_download');

// отдали клиенту $signed, потом получили обратно:
try {
    $id = $signer->unsign($signed, 'file_download');
    // $id === '42'
} catch (BadSignatureException $e) {
    // подменили значение, соль или формат строки
}

Без try/catch любое битое значение валит весь хит исключением. На границе HTTP (query/cookie) ловите исключение и отвечайте 400/403 по своей политике — не «глушите» его пустым catch без лога.

validate($value, $signature, $salt) проверяет пару значение+подпись и возвращает bool, не бросая исключение при неверных типах аргументов (вернёт false). Для целой упакованной строки обычно удобнее unsign.

Какая соль и какой ключ должны совпасть при проверке?

Соль задаёт пространство имён подписи. Это не пароль конечного пользователя. Одна и та же строка 42 с солью file_download и с солью order_view даёт разные подписи. При unsign соль должна быть той же.

Ограничение из реализации getKey: если соль не null, она обязана совпадать с регулярным выражением ^[a-zA-Z0-9_.-]{3,50}$. Иначе сразу BadSignatureException с текстом про malformed salt. Кириллица, пробел, слишком короткая соль (ab) или слишком длинная — частая причина «у меня на стенде работало», когда соль собрали из человекочитаемого заголовка.

Ключ подписи:

  1. Явно: $signer->setKey('...') — одна и та же строка на sign и unsign (и на всех серверах кластера).
  2. По умолчанию: опция main / signer_default_key через Config\Option; если пусто — ядро генерирует значение через Security\Random и сохраняет в Option. К ключу дописывается crypto_key из секции crypto в .settings.php / Configuration, если он задан.

Практический вывод: смена crypto_key или сброс signer_default_key делает недействительными старые подписанные ссылки. Перед ротацией ключа заложите окно, когда принимаются оба ключа, либо смиритесь с массовым «Signature does not match».

Когда брать TimeSigner вместо Signer?

TimeSigner::sign($value, $time, $salt = null) добавляет метку истечения. Формат: {message}{separator}{expired timestamp}{separator}{signature}. Второй аргумент — unix-timestamp или строка в духе strtotime (+1 hour, +1 day).

<?php
use Bitrix\Main\Security\Sign\TimeSigner;
use Bitrix\Main\Security\Sign\BadSignatureException;

$signer = new TimeSigner();
$signed = $signer->sign('42', '+15 minutes', 'file_download');

try {
    $id = $signer->unsign($signed, 'file_download');
} catch (BadSignatureException $e) {
    // битая подпись ИЛИ срок вышел - смотрите текст $e->getMessage()
}

unsign проверяет и подпись, и время. Просрочка даёт сообщение вида Signature timestamp expired (...). Это всё ещё BadSignatureException, не отдельный класс — ветвить логику можно по тексту/префиксу сообщения или логировать целиком.

Когда нужен только контроль «строку не подменили» без срока — берите Signer. Ссылки на download, invite и reset-код с коротким окном удобнее закрывать через TimeSigner.

Чем отличаются Signer и TimeSigner

Критерий Signer TimeSigner
Назначение Целостность строки Целостность + срок жизни
sign sign($value, $salt = null) sign($value, $time, $salt = null)
Формат message + sep + signature message + sep + expiry + sep + signature
Просрочка Нет Да, через unsign
Тип ошибки BadSignatureException Тот же класс (+ текст про timestamp)
Типичный сценарий Signed cookie / anti-tamper id Временная ссылка, OTP-окно

Оба класса умеют pack / unpack, setSeparator, setKey, getSignature, validate.

unpack, separator и формат строки

Разделитель по умолчанию — .. Его можно сменить через setSeparator, но тогда и sign, и unsign должны жить с одним разделителем. Если в самом сообщении есть тот же символ, unpack режет справа: при limit = 2 (по умолчанию) последняя точка отделяет подпись, всё левее остаётся сообщением. Для TimeSigner частей больше — сначала отрезается подпись, затем timestamp.

unpack($value, 0) разбирает все сегменты. Если разделителя нет — снова BadSignatureException («Separator not found in value»).

Практические ловушки:

  1. Двойное urlencode / обрезка query на прокси — подпись ломается.
  2. Смена separator «для красоты» только на одной стороне.
  3. Подпись ожидают в hex; ручная склейка «своим» base64 без sign не пройдёт unsign.

Сборку URL удобно делать через Web\Uri, чтобы не потерять кодирование параметра.

Типичные BadSignatureException и как их ловить

Симптом / текст Вероятная причина Что проверить
Separator not found in value Пришла неподписанная строка или обрезали хвост Цепочка передачи, логи proxy, длина query
Signature does not match Другая соль, другой ключ, подмена message Соль 1:1; setKey/Option/crypto_key на всех нодах
Malformed salt… Соль вне [a-zA-Z0-9_.-]{3,50} Длина 3-50, латиница/цифры/_/./-
Хвост не hex (Signature must be hexadecimal string) Повреждён хвост подписи Кодирование URL, копипаст из почты
Просрочка (Signature timestamp expired) Окно TimeSigner вышло TTL при sign, часы серверов (NTP)

Шаблон на границе запроса:

<?php
use Bitrix\Main\Application;
use Bitrix\Main\Security\Sign\TimeSigner;
use Bitrix\Main\Security\Sign\BadSignatureException;

$request = Application::getInstance()->getContext()->getRequest();
$raw = (string)$request->get('t');

$signer = new TimeSigner();
try {
    $payload = $signer->unsign($raw, 'file_download');
} catch (BadSignatureException $e) {
    // залогировать класс + message без полного токена, если в message есть ПДн
    CHTTP::SetStatus('403 Forbidden');
    return;
}

// дальше - свои проверки ACL по $payload

Не логируйте сырой подписанный токен целиком в публичные каналы, если внутри есть персональные данные: подпись не шифрует содержимое.

Пример: одноразовая ссылка с TTL

Сценарий: отдать ссылку на скачивание файла на 20 минут. Id файла не секрет, но подмена id на чужой номер должна отсекаться подписью; поздно пришедшая ссылка — сроком.

<?php
use Bitrix\Main\Security\Sign\TimeSigner;
use Bitrix\Main\Web\Uri;

function buildDownloadUrl(int $fileId): string
{
    $signer = new TimeSigner();
    $token = $signer->sign((string)$fileId, '+20 minutes', 'file_download');

    $uri = new Uri('/local/download.php');
    $uri->addParams(['t' => $token]);
    return $uri->getUri();
}

function resolveDownloadToken(string $token): int
{
    $signer = new TimeSigner();
    $id = $signer->unsign($token, 'file_download');
    return (int)$id;
}

После успешного unsign всё равно проверьте права текущего пользователя на файл. Подпись подтверждает только то, что строку выписали мы и срок ещё действует. Право читать заказ проверяйте отдельно.

Для связки с настройками ключа см. Config\Option. Если нужна единая обработка успеха/ошибок в сервисах модуля — Result и Error (сам Signer бросает исключение, Result — соседний контракт).

Нужен разбор конкретного модуля или аудит токенов на проде — напишите через karoche.ru.

Чеклист перед выкладкой на прод

  1. Соль одинакова на sign и unsign, формат [a-zA-Z0-9_.-]{3,50}.
  2. Ключ: либо общий setKey, либо стабильные Option + crypto_key на всех серверах.
  3. Для TTL выбран TimeSigner, срок адекватен сценарию, часы серверов синхронизированы.
  4. На входе HTTP — try/catch на BadSignatureException, без падения хита.
  5. После unsign — отдельная проверка прав на сущность.
  6. Токен в URL не ломается кодированием; для сборки используйте Uri.
  7. Ротация ключа спланирована: старые ссылки перестанут проходить unsign.
  8. В логах — причина исключения, не обязательно полный токен с ПДн.

Источники

Полный набор заметок по Битрикс и смежному стеку — на karoche.ru.