суббота, 30 ноября 2019 г.

Блочный шифр ГОСТ "Кузнечик" -- оптимизация

Речь идет об оптимизации алгоритма блочного шифра ГОСТ "Кузнечик". Я давно отложил задачу на сладкое, а тут время появилось ее доделать.
В основе шифра Кузнечик лежат две операции. 1) нелинейное биективное преобразование - табличная подстановка. 2) Что-то вполне линейное, в смысле алгебра линейная в конечном поле, на основе арифметики Галуа, с редуцированием по полиному 0x1С3.

Я увидел возможность преобразовать алгоритм, сделать его быстрее.
Итак структура алгоритма такова [ГОСТ 34.12-2015]:
E[K] = X[K10]LSX[K9]... LSX[K2]LSX[K1](a)
Я пропущу описание того, что сделано в первой серии. Я умею выражаться длинными векторами, для этого использую векторные расширения языка Си. Блочный шифр -- это преобразование вектора состоящего из 16 элементов разрядностью 8 бит. Для этого использую описание типа:

typedef uint8_t uint8x16_t __attribute__((__vector_size__(16)));

Алгоритм защифровывания выглядит так
uint8x16_t kuzn_encrypt(KuznCtx* ctx, const uint8x16_t a)
{
    uint8x16_t S = a ^ ctx->K[0];
    int i;
    for (i=0; i<9; i++){
        S = LS(S) ^ ctx->K[i+1];
    }
    return S;
}

Шифрование -- это вот такой алгоритм, внутри которого есть функция LS, всего девять циклов по развернутому ключу {K1,K2, ...K10}.
Зашифровывание -- это последовательное применение преобразований:
X[K1], S, L, X[K2], S, L, ... X[K9], S, L, X[K10]
Преобразование X[K](a) = K ⊕ a
Преобразование S -- это таблица подстановок -- "нелинейное биективное преобразование".

uint8x16_t LS(uint8x16_t a)
{
    int i;
    for(i=0; i< 16;i++) a[i] = sbox[a[i]]; // -- подстановка, 
    uint8x16_t v = R16(a); // -- то самое преобразование L
    return v;
}

Вот добрались до сути. КАК оптимизировать преобразование R16?
Преобразование R выполняется над векторами 16 элементов по 8 бит и содержит 16 циклов.
Кроме всяких пермутаций это преобразование содержит много операций умножения в конечном поле GF(2) с полиномом p(x) = x8 + x7 + x6 + x + 1, или Px = 0x1C3.
Путем долгих мучительных преобразований можно записать, что каждый элемент входного вектора зависит от каждого элемента выходного вектора. Всего 16 наборов констант для каждого элемента. vi = SUM(aj * LMTij) . LMT - это матрица коэффициентов, которую по ходу пришлось пересчитывать и транспонировать, чтобы преобразование стало выглядеть линейно.

uint8x16_t GMULv16(uint8x16_t L, uint8x16_t a);
uint8x16_t R16(uint8x16_t a)
{
    int i;
    uint8x16_t v = {0};
    for(i=0; i< 16;i++) {
        v ^= GMULv16(LMT[i], a[i]); // -- векторное умножение в поле Px = 0x1C3
    }
    return v;
}
Операйця GMULv16 выполняет умножение каждого элемента вектора с редуцированием по полиному 0x1C3.
На этом этапе я остановился в прошлый раз. Большим достижением казалость то, что алгоритм удалось сделать без ветвления и с ипользованием векторных инструкций. GMULv16 можно заменить на 16 таблиц по 256 значений по 16 байт каждое, 64кБайт. Но это не наш путь.

В цикле можно использовать умножение без переноса, а финальное редуцирование выполнять за пределами цикла.
vi = SUM((aj * LMTij) mod P(x)) =  SUM(aj * LMTij) mod P(x)

В результате получаем такой алгоритм:

uint8x16_t R16(uint8x16_t a)
{
    int i;
    uint16x16_t vh = {0};// -- 16 элементов разрядностью 16 бит. 256 бит.
    for(i=0; i< 16;i++) {
        vh ^= CL_MULv16(LMT[i], a[i]); // -- векторное умножение без переносов
    }
    uint8x16_t v = RR (vh, Px);// -- финальное редуцирование по полиному P(x),
    return v;
}
Размер таблицы - 16 элементов по 16 байт, 256 байт. В цикле получаем умножение 128 бит на 8 бит. Это умножение можно представить 4я инструкциями умножения полиномов в поле разрядностью 64 бит.

Может быть есть компромис между размером таблицы и безумным умножением?
Я так понимаю, что большие таблицы медленно обрабатываются, потому что в кеш память не влезают. Если мы делаем алгоритм для контроллера, то таблицы 65кБайт не лезут во флеш память, не лезут в память оперативную, сильно увеличивают объем кода. Обработка таблицы будет идти со скоростью работы памяти, а не со скоростью ядра процессора. Для встроенных приложений нужен компактный код. На нашей целевой архитектуре есть инструкция полиномиального умножения (ARM Cortex-A8+ NEON, ARMv8, Intel Core+AVX), но в контроллерах ARMv7e-m ничего такого нет, вынужденно используем таблицы. Нам понадобятся оба варианта алгоритма. Таблица не лезит в память, поэтому я сделал две таблицы, одна - это таблица умножения на числа 0,1,2,3...15, младшие биты числа. Вторая тоже таблица умножения на числа 0,16,32,64...256, - умножение на старшие биты числа.

uint8x16_t R16(uint8x16_t a)
{
    int i; 
    uint8x16_t v = {0}; 
    uint8x16_t a0 = a & 0xF,  a1 = (a >>4) & 0xF; 
    for(i=0; i< 16;i++) { 
        v ^= GMULv16_L[i][a0[i]] ^ GMULv16_H[i][a1[i]]; 
    }
    return v; 
}

В этом варианте умножения используются 32 таблицы по 16 значений по 16 байт, 8кБайт. Надо проверить, какой из двух вариантов быстрее: с подстановкой 8кБайт или с безумным умножением...
Проверил. Сильно зависит от процессора. На 3-м поколении Intel Core-i7 умножение без переносов работает крайне медленно, в пять раз медленнее чем табличный вариант. На 7-м поколении Core-i7 умножение работает в два раза медленнее чем табличный вариант. Разница есть. Табличный вариант позволяет получить скорость зашифровывания около 8 Мбайт/сек. Это все равно медленно.

[11.12.2019] Дальше я исследую возможность переноса алгоритма с умножением полиномов на векторные инструкции ARM NEON. У меня под рукой есть кросс-компилятор GNU ARM Embedded Toolchain, GCC. И есть под рукой плата BeagleBone, на которой могу запустить полученный код. Платформа содержит процессор ARM Cortex-A8 с архитектурой ARMv7-A и поддерживает инструкции NEON.
В системе команд NEON имеется инструкция полиномиального умножения VMULL.P8, которая оперирует с векторами poly8x8_t -- 8 элементов по 8бит. Инструкция производит результат - вектор 128 бит, 8 элементов по 16 бит. Подробнее см. [NEON™ Version: 1.0 Programmer’s Guide] на сайте ARM.

Мой код выглядит так:

#include <arm_neon.h>
uint8x16_t R16(uint8x16_t a)
{
    poly16x8_t v0 = {0};
    poly16x8_t v1 = {0};
    int i;
    for(i=0;i<16;i++){
        poly8x8_t  a8 = vdup_n_u8(sbox[a[i]]);// размножаем значение на все элементы
 poly8x16_t p8 = LMT[i];
        v0 ^= vmull_p8(a8, vget_low_p8 (p8));  // младшая часть вектора poly8x8
        v1 ^= vmull_p8(a8, vget_high_p8(p8));// старшая часть вектора poly8x8
    }
    /// редуцирование вынесли из цикла
    . . .
}
В коде использую псевдофункции, которые описаны в заголовке и в результате генерируют инструкции NEON. Исследую возможные комбинации ключей компилятора, чтобы задействовать инструкции NEON:
$ arm-eabi-gcc -print-multi-lib
Нахожу подходящую комбинацию ключей:
$ arm-eabi-gcc -march=armv7-a+fp -mcpu=cortex-a8 -mfpu=neon-vfpv4 \
  -mfloat-abi=hard -O3 -S -o - kuzn.c
Содержимое цикла преобразцется в набор инструкций

        vdup.8   d18, r3        -- размножить значение 8x8
        vld1.64 {d24-d25}, [r1:64]!  -- загрузить LMT 8x8x2
        vmull.p8 q13, d18, d24  -- умножить вектора 8x8 
        vmull.p8 q9,  d18, d25  -- умножить вектора 8x8
        veor     q8,  q8,  q13  -- исключающее ИЛИ 16x8
        veor     q11, q11, q9   -- исключающее ИЛИ 16x8
Ничего лишнего, одно умножение - одна инструкция процессора. Все инструкции используют вектора 8x16, умножение проивзодится над векторами 8x8, результат умножения имеет размерность 16x8. Не путать - умножение проиводится без переносов.
Можно ли считать этот результат хорошим. Не знаю. Объективно надо замерить скорость и сравнить результат. Код получился компактный, но не факт что инструкция умножения будет выполняться быстрее, чем обращение к памяти. Делаю 1Миллион циклов зашифровывания и замеряю время -- получаю результат = 2.5 секунды, на платформе beaglebone Texas Instrumens Sitara AM335x. Это получается скорость шифрования 6.4Мбайта/сек. Замеряю результат табличного варианта алгоритма зашифровывания = 4.2 секунды. Мы выиграли время и место. Дальше за счет разворачивания цила удалось повысить скорость зашифровывания при использовании умножения до 7.4Мбайт/сек. Это ХОРОШИЙ результат!

вторник, 19 ноября 2019 г.

Резюме 2019/2020

Ищу работу!

Чего умею?
Умею быть заказчиком на строительстве и эксплуатации. Умею быть главным инженером, умею быть главным инженером проекта. Умею проектировать и собирать электрощиты, налаживать чиллеры, умею рассчитывать и комплектовать вентиляцию, тепловые пункты, отопление, водоснабжение, электрику и переделывать всю инженерию за всеми подрядчиками слева-направо, находить ошибки в проектах в любых разделах. Умею тратить деньги заказчика миллионами.

Люблю программировать. Умею писать операционные системы, умею писать криптографию, умею на время создавать безумное количество электроники для автоматизации, чтобы сразу работало.
Умею вести производство военной техники, умею разрабатывать военную технику с гарантией 7 лет и сроком службы до 40 лет.
Умею заниматься снабжением военных и гражданских проектов практически любых. Умею работать со спец. счетами и гос контрактами. Умею отбиваться от налоговых и прокурорских проверок, хотя это просто везение, научился выживать в условиях травли. Умею сохранять данные в облаках и на выделенных серваках, шифровать и нарезать мелко-мелко большие данные. Умею жонглировать серверами, когда их выносит ФСБ или МВД. Умею писать обработку протоколов ФТС. Умею писать блок чейны и архивы. Умею разбирать архивы документов XML со скоростью ... с любой скоростью. Имею опыт разработки корпоративных информационных систем на этих технологиях.

Кажется, разучился проектировать процессоры и программировать матрицы, лет 8 этим не занимался. Зато знаю толк в оптимизации под 512 битные архитектуры и тернарные операции.

Почему-то не умею извлечь прибыль из этого.
Скучаю, ищу работу...Безумно устал от всей суеты. В отпуск хочу, в увольнительную. Хочу программы писать со скуки и воспитывать программистов. Достаточно одного.


Резюме 2011

Ищу работу!
Чего умею?
  • Умею быть преподавателем. Лет 5 отработал преподавателем электроники. Создал свой собственный курс по микросхемотехнике в Политехническом университете.
  • Могу управлять небольшим коллективом разработчиков.
  • Умею проектировать электронику: измерительную, цифровую и силовую. 
  • Умею писать встроенное программное обеспечение для ARM, 8051.
  • Умею производить спроектированную электронику. Умею организовать контрактное производство электроники штучное, мелко серийное и серийное.
  • Умею создавать сети для организаций, на 100-1000 рыл. Вообще-то умею создавать виртуальные транс-национальные корпорации не слезая со стула, вернее проектировать и настраивать аппаратуру и ПО для них. Умею в одиночку обслуживать 40 Кисок (CISCO) и десяток серверов. Хорошо знаком с GNU/Linux, FreeBSD, Oracle Solaris. Могу дописывать и исправлять код каждой из них, если понадобится.
  • Знаю толк в информационной безопасности. Могу настроить инфраструктуру организации, включая сервисы e-mail, web, vpn, ip телефонию и многое другое. 
  • Программировать люблю, на С. Умею проектировать все, что касается ядра операционной системы. Могу написать свою операционную систему с нуля. Так и сделал. Файловую систему для SSD. Люблю описывать сетевые протоколы, люблю писать интерпретаторы разных языков. Умею проектировать алгоритмы управления роботами. Умею расчитывать траектории движения без использования вещественных чисел с компенсацие ошибок округления. Умею писать реализации криптографических алгоритмов. Умею все это реализовать в аппаратуре программируеых матриц. Могу процессор спроектировать, правда не за один месяц.
  • Могу писать пркиладное программное обеспечение, имею богатый опыт разработки ПО для систем реального времени.
  • Могу эксперименты ставить по физике полупроводников, знаю толк в террагерцовой оптоэлектронике, правда лет 10 этим не занимался.
Почему-то не умею извлечь прибыль из этого, странно.
Скучаю, ищу работу...

Упражнения с BACnet - журнал операций

Я когда начинал описывать протокол BACnet, сделал такой сетевой "драйвер", который загружает формат Wireshark PCAP и подает на вход службы разбора пакетов APDU. Это позволило написать и отладить разбор форматов ASN кодирования BACnet.
С тех пор прошло много времени. Много кода... Сейчас Мне понадобилось сохранять конфигурацию контроллера в ответ на команду Re-инициализации. Вообще, как себе представляешь можно работать в контроллере с базой данных объектов? И при этом требуется сохранять конфигурацию между сессиями, при перезагрузке. Я придумал, что надо писать во флеш журнал операций модификации данных. Просто так, линейно вперед, пока место не закончится. Когда контроллер просыпается после перезагрузки он должен накатить журнал операций. Журнал, чтобы не плодить разные сущности я решил паковать прямо командами протокола, транзакциям. Например, транзакция понятная - создать объект, дописать свойства к объекту. По сути мы можем сохранять все операции записи типа WriteProperty, прямо из буфера обмена без изменения писать в журнал - во флеш. Вот такая замечательная идея.
Как отладить замечательную идею?
Журнал во флеш, флеш в контроллере. Контроллер перегружается... Не удобно.
Кроме контроллера есть сервер сбора данных, роутер BACnet. Предлагаю сохранить его конфигурацию в файл и поизучать каким-нибудь инструментом. Нужна отладка для журнала файловой системы.
Берем приложение, пригодное для отладки, для рассматривания журналов. Да вот оно - Wireshark. В итоге я пишу функцию, функционал, для записи данных не во флеш, а в файл со структурой PCAP и изучаю, что получилось в итоге. А в итоге имею последовательность команд протокола CreateObject, CreateObject, CreateObject... и на этом выводе примерно за день отлаживаю функцию экспорта объектов из базы данных в журнал.

База данных - это структура типа дерево объектов.
Foreach (DeviceInfo->ObjectTree, (Callback) save_object_cb, to_file);
Т.е. описываем некоторую процедуру обхода дерева с записью каждого элемента в файл/журнал. Так выглядит сериализация дерева.
Каждая запись состоит из шапки::{длина данных, тип записи}, пакета данных, контрольной_суммы. После чего можно записать следующую запись. В шапку можно записать и тип записи, в нашей концепции - это тип сервиса - CreateObject, DeleteObject, WritePropertyMultiple.
По сути нам не нужен этот идентификатор сервиса, потому что CreateObject без параметров можно расценивать как Delete, А операцию повторного создания объекта расценивать, как WritePropertyMultiple. Писать в журнал поток команд протокола подкупает своей простотой.
При ре-инициализации устройства через перезагрузку можно переписать журнал заново. При загрузке просто накатить этот журнал.

Есть проблема: как минимизировать объем данных в журнале, как делать инкрементную запись -- писать только те параметры, которые изменились с прошлой версии базы.
[07.01.2020]
Написал вывод в журнал, с учетом файлов. Получилось две операции: CreateObject, AtomicWriteFile.
Нашел свой старый проект, журналируемая файловая система для флеш. Тогда мы использовали четыре вида полей: APPEND - дописывание файлов, ATTRIB - дописывание/изменение атрибутов, CREATE - создание, DELETE - удаление, SKIP - технологическая операция - заполнение пробелов, для выравнивания на блок записи.
Сейчас думаю использовать старые идеи только расширить понятие файловой системы до объектов. CREATE -- создание объектов CreateObject, ATTRIB - запись атрибутов в формате WritePropertyMultiple, APPEND - дописывание файлов в форме AtomicWriteFile, DELETE - пометить на удаление из базы DeleteObject.

среда, 6 ноября 2019 г.

ARMv7m -- операции BFI и BFX и битовые строки

В приложении к криптографии я задался вопросом как "правильно" писать исходный код на языке Си, чтобы компилятор использовал инструкции BFI (Bit Field Insert) и BFX(Bit Field Extract).

Мой алгоритм берет 4 бита и использует их в качестве индекса массива. В примере я описываю "нелинейное биективное преобразование" по таблице подстановок (ГОСТ 34.12-2015 п.5.1.1).
Для отладки алгоритма я использую компиляцию в ассемблер и рассматриваю, какими командами компилятор выражается.
> gcc -march=native -O3 -o - -S magma.c
-- на экран выводится ассемблерный код оптимизированный под мой процессор
Или под целевую платформу ARMv7e-m
$ arm-none-eabi-gcc  -mthumb -mcpu=cortex-m4 -march=armv7e-m -mfloat-abi=hard -mfpu=fpv4-sp-d16 -o - -O3 magma.c -S

Исходно пишу на Си, рассматриваю результат.
Пишу определение, которое соответствует операции BFX

#define BEXTR(x, n, len) (((x) >> (n)) & ((1 << (len))-1))
В исходнике пишу так:

   for (i=0; i<8; i++){
       s = sbox[i][BEXTR(a,(i*4),4)];
       r |= (s & 0xF) <<(i*4);
   }
sbox - это таблица подстановок.
В ассемблерном коде возникает команда
ubfx...
Но мне никак не удается подобрать обратную операцию.
Пишу определение
#define BFI(x, y, n, len) x = ((x) & ~(((1 << (len))-1)<<(n))) | ((y & ((1 << (len))-1))<<(n))
Но в результате компилятор НЕ использует инструкцию bfi, определение НЕ работает.
Смотрю в документацию (Arm C Language Extensions, ACLE Q2 2019) нахожу пояснение, инструкция BFI описывается средствами языка Си, т.е. должно быть соответствующее ей выражение.
Через некоторое время, дошло, что в расширении языка С бывают свои битовые строки. Такое вот описание битовых полей (см код ниже) позволило однозначно задействовать инструкцию извлечения битовой строки и последующее помещение битовой строки обратно в регистр.

uint32_t T(uint32_t a)
{
    register union {
      struct {
        uint32_t u0:4;
        uint32_t u1:4;
        uint32_t u2:4;
        uint32_t u3:4;
        uint32_t u4:4;
        uint32_t u5:4;
        uint32_t u6:4;
        uint32_t u7:4;
      };
      uint32_t x;
    } r;
    r.x  = a;
    r.u0 = sbox[0][r.u0];
    r.u1 = sbox[1][r.u1];
    r.u2 = sbox[2][r.u2];
    r.u3 = sbox[3][r.u3];
    r.u4 = sbox[4][r.u4];
    r.u5 = sbox[5][r.u5];
    r.u6 = sbox[6][r.u6];
    r.u7 = sbox[7][r.u7];
    return r.x;
}
Это и есть биективное преобразование согласно ГОСТ, именно в таком виде оно попало в мою реализацию. -- Магия!!

В результате компилятор создает такой код:

   ubfx r2, r0, #16, #4   -- загрузить битовое поле
   add  r2, r2, r3
   ldrb r2, [r2,#64] @ zero_extendqisi2 -- загрузить байт из таблицы
   bfi  r0, r2, #16, #4   -- вставить битовое поле

CRISP 1.0 -- Протокол защищенного обмена для индустриальных систем


Протокол не важен. Важно что это первый нормальный набор векторов для Magma (ГОСТ 34.12-2015)в режиме генерации имитовставки (IMIT) и в режим гаммирования (CTR) для данных не выровненных. Сами режимы описаны в документе (ГОСТ 34.13-2015).

Протокол защищенного обмена выпущен ТС26 (Технический комитет по стандартизации российской криптографии) в виде методических рекомендаций.
Основная проблема в реализации режимов блочного шифрования - Это, не глядя в чужой код, получить точное соответствие с векторами. Сразу никогда не получается.
Где-то это мои проблемы - читать не умею. Где-то проблемы разработчика - описано плохо. Но чаще всего - это неточности в описании алгоритма, который надо как-то отладить, и ошибок в реализации может быть больше одной на пути рабочей версии.
Почему путь тернист. Берем сам алгоритм блочного шифра, "Магма" (ГОСТ 34.12-2015): описан, есть примеры - тестовые вектора. Взял алгоритм, сделал реализацию, но - не работает. Нигде в стандарте не сказано, в каком представлении даны числа в тестовых векторах. Порядок следования байт может быть Little-Endian или Network (Big-Endian). С ключами шифрования - тоже самое, они могут быть в нормальном порядке, задом наперед по 64 бита, задом наперед/совсем задом наперед - все число от старшего бита к младшему вывернуто. В процессе отладки я подбираю, как правильно записать, в какой последовательности, входные и выходные данные, чтобы числа сошлись. Сходятся.
Потом беру описание режима блочного шифра (ГОСТ 34.13-2015). В тестовых векторах та же путаница не ясно, как должны сходится эти вектора с изменение порядка следования байт или без изменения. Примеры почему то даны только для случая выровненных данных. В случае с усечением или без выравнивания на 64 бита, алгоритм остается не отлаженным.


 Теоретически мы можем предположить, что алгоритм работает так:
(подготовка ключа- надо решить как его правильно выкрутить, чтобы из сетевого представления получить числовое) -- (подготовка данных из последовательного представления в числовое) -- (подать на вход ключ и данные) -- (результат выкрутить в "сетевое" представление). При этом существуют некоторые неточности изложения, в результате которых такие же неоднозначные преобразования могут содержаться внутри самого алгоритма. Парадокс в том, что при этом вектора сходятся, потому что дают те же числа только в ином порядке.

Например, смотрим определения, чем отличается "бинарное представление", "байтовое представление" и числовое. Как разгадать этот ребус. По-русски ведь написано.
x||y -- конкатенация двоичных строк x и y ... при которой левая часть - слева, а правая - справа.
Потом пишут 1||0 - вот это чего за число. или это 0x02? Или это число 0x01? Оказывается это число 0x80.Оказывается оно таким после серии проб и ошибок.
binary() -- это представление символьной строки в виде байтовой строки. -- отпад.
byte() -- это представление числа в виде байтовой строки, при котором соответствующая итоговой байтовой строке двоичная строка .... -- вынос мозга.
вот у меня есть число 0xBAD как его представить в виде байтовой строки: "0B AD" или "AD 0B". Отсюда и берутся такие неоднозначности. Числа можно представить в "сетевом" порядке или в порядке свойственном аппаратуре LE|BE.
На самом деле, если четко договориться по терминам, что такое "двоичная строка", что такое "байтовая строка", что такое "число", что такое "символьная строка", как одно в другое переводится, то проблем нет. Авторы пытаются прикрыть неточность выражения мыслей обилием математических выражений типа Zn||...||Z2||Z1||Z0 - такие выражения никак не облегчаются понимание вопроса по выравниванию данных. Биты которые однозначно слева в "байтовом представлении" могут быть как слева так и справа. Но после прочтения не всегда это ясно.

Вот например каким количеством вариантов можно описать действие
IV = LSB_32(byte(SeqNum, 6)) -- по сути это означает что мы должны взять младшие 32 бита от байтового представления числа, при генерации байтового представления нужно взять 6 байт от числа, т.е 48 бит. Пишу варианты:
uint64_t SeqNum;
uint32_t IV = htonll(SeqNum); (1)
uint32_t IV = htonll(SeqNum<<16); (2)
uint32_t IV = (SeqNum); (3)
uint32_t IV = htonll(SeqNum)>>32; (3)
...
Или вот другая формула SN = 0^5||MSB_35(byte(SeqNum,6))
Если под функцией byte() автор понимает преставление байт из числового представления в сетевое, то это запишется так:
uint64_t SN = (swap64(SeqNum<<16)) & ((1ULL<<35)-1)); (3)
uint64_t SN = ((SeqNum>>(48-35)) & ((1ULL<<35)-1)); (4)
-- я реально перебираю все эти варианты, ориентируясь всего на один признак, результат равен или нет тестовому вектору. После подбора я нахожу, что автор имел ввиду:
SN = старшие_биты(0^5)||число(MSB_35(число(SeqNum,6)). -- сшивание битовых строк/чисел.
При укладке данных SN в "байтовую строку" происходит преобразование "в сетевом" порядке следования байт.
Строка = ...|| byte(SN,5) ||... -- число SN (40бит, 5 байт) разворачивается в порядке Big-Endian, в сетевом представлении. Откуда это следует? -- Я догадался.
Можно вчитываться в документацию, пытаться понять, все ли точно выражено в описании или все таки бинарное представление итоговой байтовой строки от числового представления сдвинутого на пять бит делается как-то иначе. А если в изложении несколько таких неточностей, или я не так понял и в моей реализации неточность или ошибка.
Может автор прав, есть однозначная запись битовых строк, операция MSB применима к битовым строкам, операция byte() преобразует число в битовую строку. Проверяем определения... Я понял, все дело в том, что битовые строки почему-то неявным образом переводятся в байтовые с выравниванием по старшему биту.
При использовании блочного шифра, я почему-то упускаю: надо данные из сетевого представления переводить в числовое перед использованием функции шифрования или надо результат функции шифрования переводить из локального в сетевое представление перед использованием. Такие вот проблемы. 

Когда результат достигнут - все просто. Такие задачи я использую исключительно для тренировки. Перед началом нового проекта, я берусь реализовать какой-нибудь новый для себя криптографический алгоритм не глядя в чужие реализации, по документации. В этот раз неделю потратил.