четверг, 20 мая 2021 г.

Векторное воплощение блочного шифра ChaCha20

Блочный шифр ChaCha - это просто[RFC 8439]. На решение задачи, как векторизовать алгоритм ушло полдня. Обратил внимание, как выглядит алгоритм в [libGCrypt]. Уныло.
Алгоритм ChaCha20 строится на примитивах QUARTERROUND.

// The ChaCha Quarter Round: QUARTERROUND(a,b,c,d)
      a += b; d ^= a; d <<<= 16;
      c += d; b ^= c; b <<<= 12;
      a += b; d ^= a; d <<<= 8;
      c += d; b ^= c; b <<<= 7;

-- сложения, исключающее или и циклические сдвиги. Таких раундов по матрице uint32х4х4 получается 8 в кажом цикле и всего 10 циклов.

// Тело цикла ChaCha
      QUARTERROUND(0, 4,  8, 12)
      QUARTERROUND(1, 5,  9, 13)
      QUARTERROUND(2, 6, 10, 14)
      QUARTERROUND(3, 7, 11, 15)
      QUARTERROUND(0, 5, 10, 15)
      QUARTERROUND(1, 6, 11, 12)
      QUARTERROUND(2, 7,  8, 13)
      QUARTERROUND(3, 4,  9, 14)

Вот так выглядит тело цикла, индексы в скобках - индексы массива состояния, всего 16 переменных. Не сложно заметить, каждая переменная участвует только в двух из восьми раундах. Присмотрелся -- первые четыре раунда могут выполняться параллельно и вторые четыре раунда могут выполнятся параллельно.

// Тело цикла ChaCha - векторизация по uint32х4 128 бит.
		a += b; d ^= a; d = ROL(d,16);
		c += d; b ^= c; b = ROL(b,12);
		a += b; d ^= a; d = ROL(d, 8);
		c += d; b ^= c; b = ROL(b, 7);
		b = SHUFFLE(b, 1, 2, 3, 0);
		c = SHUFFLE(c, 2, 3, 0, 1);
		d = SHUFFLE(d, 3, 0, 1, 2);
		a += b; d ^= a; d = ROL(d,16);
		c += d; b ^= c; b = ROL(b,12);
		a += b; d ^= a; d = ROL(d, 8);
		c += d; b ^= c; b = ROL(b, 7);
		b = SHUFFLE(b, 3, 0, 1, 2);
		c = SHUFFLE(c, 2, 3, 0, 1);
		d = SHUFFLE(d, 1, 2, 3, 0);

Вот так работает! Все операции - векторные, ROL - циклический сдвиг, SHUFFLE - перестановка слов.

Между строк написано, что ChaCha20 сделал по мотивам Salsa20. Структура та же - 8 раундов и 10 циклов.

// Тело цикла Salsa20 - векторизация по uint32х4 128 бит.
		// замена столбцов x3<=>x1
		x1 = SHUFFLE(x1, 1, 2, 3, 0);
		x2 = SHUFFLE(x2, 2, 3, 0, 1);
		x3 = SHUFFLE(x3, 3, 0, 1, 2); 
		x1 ^= ROL(x0 + x3, 7);
		x2 ^= ROL(x1 + x0, 9);
		x3 ^= ROL(x2 + x1,13);
		x0 ^= ROL(x3 + x2,18);
		// замена столбцов x3<=>x1
		x3 = SHUFFLE(x3, 1, 2, 3, 0);
		x2 = SHUFFLE(x2, 2, 3, 0, 1);
		x1 = SHUFFLE(x1, 3, 0, 1, 2);
		x3 ^= ROL(x0 + x1, 7);
		x2 ^= ROL(x3 + x0, 9);
		x1 ^= ROL(x2 + x3,13);
		x0 ^= ROL(x1 + x2,18);

Также как и в предыдущем случае, в переменных состояния {x0,x1,x2,x3} живут столбцы матрицы uint32х4x4. Для подготовки данных к такому алгоритму матрицу состояния пришлось транспонировать.

Производительность

Получил результат по производительности. Применил хитрость при измерении скорости. Загрузил состояние в кеш, и загрузил код в кеш, методом запустить дважды. Получилась производительность по алгоритмам примерно равная на уровне 80 тактов на 512 байт, 10 циклов для каждого алгоритма. Измерял на Intel Core-i3 Gen10.

понедельник, 17 мая 2021 г.

Функции GHASH и POLYVAL в алгоритмах шифрования GCM и GCM-SIV

Придумал более эффективную реализацию функции умножения в поле GF128, на которой строится фукнция POLYVAL и GHASH. Делюсь..
Записал с использованием макро определений инструкций.

// Алгоритм умножения на инструкциях умжножения без переноса (PCLMUL).
__m128i gmul128r(__m128i a, const __m128i b)
{
    const __m128i  poly = _mm_setr_epi32(0x1,0,0,0xc2000000);
    __m128i  M,L,H;
    L = _mm_clmulepi64_si128(a, b, 0x00);
    M = _mm_clmulepi64_si128(a, b, 0x01);
    M^= _mm_clmulepi64_si128(a, b, 0x10);
    H = _mm_clmulepi64_si128(a, b, 0x11);
// редуцирование poly = 1.C2...01
    M^= _mm_shuffle_epi32(L, 78);
    M^= _mm_clmulepi64_si128(L, poly, 0x10);
// редуцирование 
    H^= _mm_shuffle_epi32(M, 78);
    H^= _mm_clmulepi64_si128(M, poly, 0x10);
    return H;
}
// Функция расчета кода аутентификации
__m128i POLYVAL(__m128i y, __m128i h, const uint8_t *data, int len)
{
    __m128i x;
    int i=0;
    int blocks = len>>4;
    for (i=0; i<blocks; i++){
        __builtin_memcpy(&x, data, 16); data+=16;
        y = gmul128r((y^x), h);
    }
    if (len & 0xF){
        x ^= x;
        __builtin_memcpy(&x, data, len&0xF);
        y = gmul128r((y^x), h);
    }
    return y;
}
// Функция расчета кода аутентификации
__m128i GHASH(__m128i y, __m128i h, const uint8_t *data, int len)
{
    int i=0;
    __m128i x;
    h = SLM128(h);// -- сдвиг с редуцированием, можно вынести из функции
    int blocks = len>>4;
    for (i=0; i<blocks; i++){
        __builtin_memcpy(&x, data, 16); data+=16;
        y = gmul128r((y^x), h);
    }
    if (len &0xF){
        x ^= x;
        __builtin_memcpy((uint8_t*)&x +(16-(len&0xF)), data, len&0xF);
        y = gmul128r((y^x), h);
    }
    return y;
}

Тут достижение в оптимизации фукнции редуцирования. Редуцирование выполняется по промежуточному значению M. Этот алгоритм меньше на три векторных инструкции, чем референсный [RFC8452]. Чтобы убедиться, надо лезть на github и сравнивать реализации[AES-GCM-SIV].

  • [RFC 8452] AES-GCM-SIV, Gueron, et al., April 2019
  • [AES-GCM-SIV] https://github.com/Shay-Gueron/AES-GCM-SIV
  • [1] Enabling High-Performance Galois-Counter-Mode on Intel® Architecture Processors, 2012
  • [2] Optimized Galois-Counter-Mode Implementation on Intel® Architecture Processors, 2010
  • [3] Intel® Carry-Less Multiplication Instruction and its Usage for Computing the GCM Mode, 2009-2014

среда, 12 мая 2021 г.

Алгоритм блочного шифра "Кузнечик"

Занимался оптимизацией алгоритма шифрования Кузнечик [ГОСТ Р 34.12-2015]. Последние действия могу объяснить, а откуда получены промежуточные, забываю. Надо зафиксировать знание. Чтобы восстановить логику, пишу статью. Цель статьи -- показать, как можно оптимизировать алгоритмы в конечном поле.

Цикл алгоритма состоит из трех преобразований (LSX): наложение колюча(X), табличная подстановка(S) и некоторые линейные действия в конечном поле GF(2^8) с полиномом 0x1С3.

Преобразование L состоит из множества умножений и сложений в конечном поле. Операцию L можно исключить и сделать табличной. А наоборот, нельзя ли вычислить L честно и в тоже время быстро?

L состоит из 16 циклов R замешивания и умножения. В результате можно написать произведение матрицы коэффициентов (LM) 16х16 элементов на вектор из 16 элементов: LM*(a) -- линейная алгебра в поле GF. Если в нашем распоряжении есть векторная инструкция умножения в конечном поле, то задача может быть решена.

В архитектуре ARM Neon есть инструкция полиномиального векторного умножения (vpmull_p8), которая позволяет выполнить 8 умножений параллельно, результат - вектор 128 бит состоящий из 8 произведений выполненных без переносов(полиномиальных). Чтобы вернуть результат умножения в поле, надо выполнить операцию редуцирования. Операция матричного умножения в поле GF(2^8)

// Операция  матричного умножения в поле GF(2^8)
// редуцирование вынесено из цикла
#include <arm_neon.h>
poly16x8_t v0 = {0};
poly16x8_t v1 = {0};
int i;
for(i=0; i<16; i++) {
    // размножаем значение на все элементы
    poly8x8_t a8 = (poly8x8_t)vdup_n_u8(sbox[a[i]]);
    // загружаем столбцы матрицы коэффициентов
    poly8x16_t p8 = LMT[i];
    // умножаем младшую часть вектора poly8x8
    v0 ^= vmull_p8(a8, vget_low_p8 (p8));
    // умножаем старшую часть вектора poly8x8
    v1 ^= vmull_p8(a8, vget_high_p8(p8));
}

В результате вектор {v0,v1} содержит результат матричного умножения, 16 элементов по 16 бит в двух векторных регистрах, который далее следует редуцировать до 8 бит, чтобы вернуть в поле GF(2^8).

Редуцирование выполняется по методу Баретта. В алгоритме редуцирования используется обратное число - константа баретта (0x1B5) и сам полином (0x1С3).

Редуцирование по Баретту напоминает остаток от деления.
q = a/P = a*B
r = a - q*P

В нашем случае нужно заменить операции на соответствующие операции в поле GF

Алгоритм №__. Редуцирование по Баретту B=0x1B5, P=0x1C3.
На Входе: [a1:a0]
    [q1:q0] := a1 ⊙ B -- операция умножения
    [r1:r0] := [a1:a0] ⊕ q1 ⊙ P
На выходе: r0
// редуцирование вынесли из цикла
    poly8x8_t t0;
	poly16x8_t t;
	const poly8x8_t B5 = vdup_n_p8(0xB5);
	const poly8x8_t C3 = vdup_n_p8(0xC3);
    // сдвиг вправо элементов вектора 16 бит на 8 бит с заужением
    t0 = (poly8x8_t) vshrn_n_u16((uint16x8_t)v0, 8);
    t  = vmull_p8(t0, B5);// 
    t0^= (poly8x8_t) vshrn_n_u16((uint16x8_t)t, 8);
    v0^= vmull_p8(t0, C3);// v0 := v0 - t0*P

Операция редуцирования выполняется над каждым элементом вектора v0 и v1. Результат - вектор poly16x8_t. Операция выполняется над вектором v1 - старшая часть вектора 16х16. Финально необходимо преобразовать вектор обратно к разрядности poly8x16_t

Аналогичный алгоритм может быть построен на операции умножения без переносов на платформе Intel, с использованием инструкции PCLMUL

//Матричное умножение в поле GF
    for(i=0;i<16;i+=1){
        a16[0] = sbox[a[i]];
        poly64x2_t L = (poly64x2_t)UNPACKLBW128((int8x16_t)LMT[i], Z);
        poly64x2_t H = (poly64x2_t)UNPACKHBW128((int8x16_t)LMT[i], Z);
        v0 ^= CL_MUL128(a16, L, 0x00);
        v1 ^= CL_MUL128(a16, L, 0x10);
        v2 ^= CL_MUL128(a16, H, 0x00);
        v3 ^= CL_MUL128(a16, H, 0x10);
    }

Привожу табличный алгоритм, чтобы было с чем сравнивать

//Векторное воплощение операции LSX
uint8x16_t LSX(uint8x16_t a, uint8x16_t K)
{
    poly64x2_t r0={0};
    poly64x2_t r1={0};
    int i;
    poly64x2_t bb= (poly64x2_t)(a ^ K);
    uint64_t b0 = bb[0];
    uint64_t b1 = bb[1];
#pragma GCC unroll 8
    for(i=0;i<8;i++) {
        uint64_t aa = a[i];
        r0 ^= LS[i  ][(b0>>(i*8))&0xFF];
        r1 ^= LS[i+8][(b1>>(i*8))&0xFF];
    }
    return (uint8x16_t)(r0 ^ r1);
}

Результат практический: удалось получить производительность на Intel Core (10th Gen) до 20 тактов на байт при шифровании MGM. В то время, как производительность табличного метода - в три раза выше. Теоретически можно пытаться увеличить производительность алгоритма за счет использования векторных инструкций VPCLMUL с разрядностью 256 и 512 бит. На платформе Arm Neon ожидаю сравнимую производительность с табличным методом, поскольку в цикле используется меньше инструкций. Данных по производительности на ARM пока нет.

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