Тестирование ротации ключей

Модель ключей в Iron и влияние ротации

В основе механизма Iron (в экосистеме @hapi/iron) лежит концепция «запечатывания» данных с помощью симметричного секрета. На практике используется не один ключ, а массив ключей, где:

  • первый элемент считается активным (используется для шифрования новых данных)
  • остальные остаются для расшифровки ранее созданных токенов
  • порядок элементов определяет приоритет

При ротации ключей важно обеспечить два свойства:

  • новые данные всегда шифруются актуальным ключом
  • старые данные продолжают корректно расшифровываться до завершения периода миграции

Любое тестирование ротации ключей должно проверять именно эту двойственность поведения.


Базовая конфигурация с несколькими ключами

Типичный вариант настройки выглядит так:

import Iron from '@hapi/iron';

const keys = [
  'current-secret-key',
  'old-secret-key-1',
  'old-secret-key-2'
];

const options = {
  encryption: {
    saltBits: 256,
    algorithm: 'aes-256-cbc'
  }
};

В этом наборе:

  • keys[0] — активный ключ
  • остальные — резервные для расшифровки

Ключевой сценарий: шифрование с текущим ключом

Тестирование начинается с проверки того, что сериализация использует только активный ключ.

import Iron from '@hapi/iron';

test('шифрование использует только первый ключ', async () => {
  const keys = ['key-A', 'key-B'];

  const sealed = await Iron.seal(
    { user: 'alice' },
    keys[0],
    Iron.defaults
  );

  expect(typeof sealed).toBe('string');
});

Важный момент: в Iron нельзя напрямую указать массив ключей при seal. Ротация проявляется на этапе unseal.


Проверка расшифровки с ротацией ключей

Основная цель тестов — убедиться, что данные, зашифрованные старым ключом, всё ещё читаются.

test('расшифровка работает с ротацией ключей', async () => {
  const keys = ['new-key', 'old-key'];

  const sealedWithOldKey = await Iron.seal(
    { role: 'admin' },
    'old-key',
    Iron.defaults
  );

  const unsealed = await Iron.unseal(
    sealedWithOldKey,
    {
      ...Iron.defaults,
      keys
    }
  );

  expect(unsealed.role).toBe('admin');
});

Здесь проверяется критический сценарий:

  • данные зашифрованы старым ключом
  • система уже использует новый ключ
  • расшифровка должна успешно пройти через fallback-механизм

Проверка приоритета ключей

Важно убедиться, что порядок ключей влияет только на шифрование, но не ломает расшифровку.

test('приоритет ключа влияет на шифрование', async () => {
  const keys = ['key-1', 'key-2', 'key-3'];

  const sealed = await Iron.seal(
    { scope: 'user' },
    keys[0],
    Iron.defaults
  );

  const unsealed = await Iron.unseal(
    sealed,
    {
      ...Iron.defaults,
      keys
    }
  );

  expect(unsealed.scope).toBe('user');
});

Если изменить порядок ключей и оставить только старые в массиве, расшифровка должна продолжать работать до удаления ключа из rotation set.


Симуляция ротации ключей в продакшн-условиях

Реальные системы редко переключаются мгновенно. Обычно применяется поэтапная схема:

  1. добавление нового ключа в начало массива
  2. сохранение старых ключей в списке
  3. постепенное удаление устаревших ключей

Тестирование должно моделировать эти этапы.

function rotateKeys(oldKeys, newKey) {
  return [newKey, ...oldKeys];
}

Тест:

test('эмуляция ротации ключей', async () => {
  const oldKeys = ['key-v1', 'key-v2'];

  const initial = await Iron.seal(
    { data: 123 },
    oldKeys[0],
    Iron.defaults
  );

  const rotated = rotateKeys(oldKeys, 'key-v3');

  const result = await Iron.unseal(
    initial,
    {
      ...Iron.defaults,
      keys: rotated
    }
  );

  expect(result.data).toBe(123);
});

Проверка деградации: удаление ключа из rotation set

Один из наиболее критичных тестов — поведение при удалении ключа.

test('ошибка при удалении всех подходящих ключей', async () => {
  const keys = ['new-key'];

  const sealed = await Iron.seal(
    { id: 1 },
    'old-key',
    Iron.defaults
  );

  await expect(
    Iron.unseal(sealed, {
      ...Iron.defaults,
      keys
    })
  ).rejects.toThrow();
});

Это гарантирует:

  • система не «угадывает» ключи
  • отсутствует небезопасный fallback
  • поведение остаётся детерминированным

Тестирование массовой ротации токенов

При миграции систем часто требуется проверить большое количество токенов, зашифрованных разными ключами.

test('массовая расшифровка при ротации', async () => {
  const keys = ['k3', 'k2', 'k1'];

  const tokens = await Promise.all([
    Iron.seal({ id: 1 }, 'k1', Iron.defaults),
    Iron.seal({ id: 2 }, 'k2', Iron.defaults),
    Iron.seal({ id: 3 }, 'k3', Iron.defaults)
  ]);

  const results = await Promise.all(
    tokens.map(t =>
      Iron.unseal(t, { ...Iron.defaults, keys })
    )
  );

  expect(results.map(r => r.id)).toEqual([1, 2, 3]);
});

Проверка устойчивости к частично повреждённым ключам

В распределённых системах ключи могут быть неконсистентны между инстансами. Это создаёт риск частичной расшифровки.

test('игнорирование неподходящих ключей в цепочке', async () => {
  const keys = ['valid-key', 'invalid-key'];

  const sealed = await Iron.seal(
    { flag: true },
    'valid-key',
    Iron.defaults
  );

  const result = await Iron.unseal(sealed, {
    ...Iron.defaults,
    keys
  });

  expect(result.flag).toBe(true);
});

Стратегия тестирования ротации в CI

При автоматизированной проверке важно учитывать:

  • фиксацию набора ключей для детерминизма
  • генерацию токенов с каждым ключом отдельно
  • проверку матрицы совместимости

Пример матрицы:

ключ шифрования набор ключей при расшифровке результат
k1 [k1, k2, k3] успех
k2 [k1, k2, k3] успех
k3 [k1, k2, k3] успех
k1 [k2, k3] ошибка

Тестирование регрессии при обновлении алгоритма

При смене конфигурации Iron (например, обновление defaults) важно убедиться, что ротация ключей не ломает формат сериализации.

test('совместимость после изменения defaults', async () => {
  const key = 'stable-key';

  const sealed = await Iron.seal(
    { version: 1 },
    key,
    Iron.defaults
  );

  const unsealed = await Iron.unseal(
    sealed,
    {
      ...Iron.defaults,
      keys: [key]
    }
  );

  expect(unsealed.version).toBe(1);
});

Ключевые ошибки при тестировании ротации

В практических реализациях часто встречаются ошибки:

  • использование одного ключа вместо массива
  • отсутствие тестов на старые токены
  • проверка только успешных сценариев
  • игнорирование порядка ключей
  • отсутствие проверки удаления ключей

Каждая из этих ошибок приводит к деградации безопасности при реальной ротации.