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

Дальше я сделал ровно то, что сделал бы наверное почти каждый: пошёл за Ed25519. Он компактный, быстрый, детерминированный, лежит в каждом втором SSH-ключе. В стандартной библиотеке .NET его не оказалось: ни в восьмой версии, ни в десятой. Зато в десятой уже есть постквантовые ML-DSA и SLH-DSA.

Дальше:

  • что показали замеры;

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

  • почему не жалею об этом.

Что вообще требовалось от подписи?

Задача кажется academic только на первый взгляд. Требования диктовала не криптография, а форма продукта.

Требование

Откуда взялось

Проверка без сети

лицензия должна работать на компьютере без интернета

Ноль сторонних зависимостей

проверку лицензии хочу собрать из того, что уже есть в платформе

Ключ не должен превращаться в простыню

его вставляют руками, из письма

Ротация ключей

если приватный ключ утечёт, нужно уметь выпустить новый, не ломая старые лицензии

Проверка не тормозит запуск

она происходит не один раз за сеанс

Последний пункт стоит пояснить, потому что он не так очевиден. Я сознательно не стал делать в программе единый флаг «пользователь оплатил»: такой флаг снимается одним хак патчем в памяти. Для защиты от этого подпись перепроверяется в каждой точке, где платная функция реально исполняется. То есть проверок за сеанс не одна, а десятки, и вопрос «сколько стоит одна проверка» превращается из теоретического в практический.

Идёшь за Ed25519 — и упираешься в рантайм

Первым делом я полез проверять, что вообще есть в System.Security.Cryptography. Проверять решил не по памяти и не по статьям — рефлексией по самой сборке, в двух версиях рантайма сразу.

Факт-чек: что рантайм выставляет наружу

var asm = typeof(ECDsa).Assembly;
Console.WriteLine(asm.GetName().Name + " " + asm.GetName().Version);
// какие асимметричные примитивы вообще доступны
foreach (var t in asm.GetExportedTypes())
if (t.IsAbstract && typeof(AsymmetricAlgorithm).IsAssignableFrom(t))
Console.WriteLine(t.Name);
// какие кривые библиотека знает по имени
foreach (var p in typeof(ECCurve.NamedCurves).GetProperties())
Console.WriteLine(p.Name);

Результат одинаково скучный и одинаково показательный в обеих версиях.

Что искали

.NET 8.0.28

.NET 10.0.9

Типы со словом «25519»

нет ни одного

нет ни одного

Асимметричные примитивы

DSA, ECAlgorithm, ECDiffieHellman, ECDsa, RSA

ровно те же

Именованные кривые

nistP256, nistP384, nistP521 и 14 brainpool

ровно те же

Постквантовая подпись

нет

ML-DSA, SLH-DSA, ML-KEM, композитные

Попытка получить кривую по имени вежливо объясняет, что мне здесь не рады. Формулировка, кстати, лукавая: дело не в «этой платформе», а в том, что такого примитива нет в API вообще.

Ответ на попытку создать Ed25519

ECDsa.Create(ECCurve.CreateFromFriendlyName("Ed25519"))
→ PlatformNotSupportedException: The specified curve ‘Ed25519’
or its parameters are not valid for this platform.

Дальше я полез в трекер рантайма — и там оказалось интереснее, чем в коде. Предложение добавить Ed25519 и Curve25519 создано 19 июня 2015 года. Актуальное предложение API висит с 28 декабря 2021 года, у него 53 комментария, метка api-approved и веха 11.0.0. То есть API согласован, но появится он в одиннадцатой версии, а мне подписывать ключи нужно было в восьмой.

Вот это поворот: подпись, устойчивую к квантовому компьютеру, в рантайм завезли раньше, чем ту, которой пятнадцать лет и которая лежит у половины читателей в ~/.ssh!

Что сложного: «просто возьми пакет»?

Очевидное возражение: возьми библиотеку и не занимайся ерундой. Я и попробовал встроенный и наиболее распространённые.

Вариант

Что приезжает вместе с ним

Встроенный ECDSA

ноль байт, он уже в рантайме

BouncyCastle 2.6.2

одна управляемая сборка 4,70 МБ, работает везде одинаково

NSec 24.4.0

обёртка 92 КБ плюс нативная libsodium под каждую платформу: 10 файлов, суммарно 4,08 МБ

С NSec выяснилась деталь, которая мне “понравилась” даже больше цифр. Свежая версия пакета отказалась ставиться в проект на .NET 8:

Ответ NuGet на попытку взять последнюю версию

error NU1202: Пакет NSec.Cryptography 26.4.0 несовместим с net8.0.
Пакет NSec.Cryptography 26.4.0 поддерживает: net9.0, net9.0-ios18.0, ...
  

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

З.Ы. по Фрейду. Мой дистрибутив весит 63,7 МБ, потому что собран со встроенным рантаймом. На этом фоне лишние 4,7 МБ — это 7 %, и логически возникающий аргумент против: «раздувает дистрибутив» не применим в данной ситуации. Почему? Встроенная криптография в дистрибутиве уже лежит на машине пользователя как часть рантайма: взяв её, я не добавляю в проверку лицензии ни одного нового и не понятного “участника”. Сторонний пакет — это, наоборот, ещё один объект, за которым надо следить: обновлять, смотреть его уязвимости, помнить про его собственный график поддержки платформ (что мы только что и увидели) и проверять, как он переживёт планируемую обфускацию — сторонние сборки на ней спотыкаются охотнее. Мне не хотелось бы однажды разбираться, почему платный уровень перестал включаться после обновления чужой библиотеки.

Замеры: кто быстрее?

Хватит теории и скучной лирики — берём и меряем. Я взял четыре варианта и прогнал на своей рабочей машине: AMD Ryzen 7 5800H, Windows 10, .NET 8.0.28, сборка Release. Полезная нагрузка — 39 байт, ровно столько в моём формате ключа. Прогрев 2 000 итераций, замер 20 000, лучший результат из пяти прогонов.

Как мерил (сокращённо)

var payload = RandomNumberGenerator.GetBytes(39);
using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var sig = ec.SignData(payload, HashAlgorithmName.SHA256);
for (int i = 0; i < 2000; i++)              // прогрев JIT
ec.VerifyData(payload, sig, HashAlgorithmName.SHA256);
var sw = Stopwatch.StartNew();
for (int i = 0; i < 20000; i++)
ec.VerifyData(payload, sig, HashAlgorithmName.SHA256);
sw.Stop();
Console.WriteLine(sw.Elapsed.TotalMilliseconds / 20000 + " мс на проверку");

Алгоритм

Подпись

Проверка

Размер подписи

Публичный ключ

ECDSA P-256, встроенный

187 мкс

191 мкс

64 Б

64 Б

RSA-2048, встроенный

505 мкс

16 мкс

256 Б

294 Б

Ed25519, BouncyCastle

36 мкс

77 мкс

64 Б

32 Б

Ed25519, NSec + libsodium

34 мкс

91 мкс

64 Б

32 Б

Смотрим на колонку «проверка» — и видим, что выбранный мной вариант ECDSA P-256 проиграл обоим. Ed25519 проверяет в 2,5 раза быстрее, RSA-2048 — в двенадцать раз быстрее, потому что проверка у RSA это возведение в маленькую открытую экспоненту. Подписывает RSA, наоборот, медленнее, но подпись происходит в моем случае один раз на лицензию, по этому на эту разницу в общем то можно и не смотреть.

И тут же цифра, которая ставит всю таблицу на место. Первая криптографическая операция в свежем процессе (создание ключа и подпись) занимает 6,81 мс: в тридцать пять раз дольше «горячей» проверки. То есть всё, что я могу выиграть выбором алгоритма, тонет в разовой инициализации при запуске. Разница между 191 и 77 микросекундами — это разница между «незаметно» и «незаметно».

Почему выбор решала длина строки, а не микросекунды

А теперь то, что и определило выбор способа кодировки. Ключ живёт в письме, его копируют, а иногда перепечатывают руками. Кодирую в Base32, а не в более компактный Base64, намеренно: в алфавите Base32 нет строчных букв и нет пар-двойников вроде 0 и O, 1 и l, так что ключ можно продиктовать по телефону и не получить в ответ: «не подходит». Считаем: 39 байт данных плюс подпись, группы по пять символов через дефис.

Алгоритм

Подпись

Всего байт

Base32

Готовый ключ

ECDSA P-256 или Ed25519

64 Б

103

165

202 символа

RSA-2048

256 Б

295

472

571 символ

RSA-3072

384 Б

423

677

817 символов

Пятьсот семьдесят один символ — это уже не лицензионный ключ, это простыня на пол-экрана! Значит, RSA отпадает не по производительности, по которой он как раз хорош, а по внешнему виду письма. Остаются два варианта с подписью в 64 байта: тот, которого в рантайме нет, и тот, который есть.

Сразу оговорюсь, чтобы не выдавать нужду за добродетель: 202 символа — это тоже много. Красивого ключа вида ABCD-1234-EFGH здесь не будет ни при каком выборе алгоритма и вот почему. В оффлайновой схеме ключ обязан нести в себе всё: и данные лицензии, и подпись, которой эти данные скреплены, попросту программе больше неоткуда взять правду, у неё нет ни базы, ни сети. Подпись меньше 64 байт при вменяемой стойкости не бывает, а значит нижняя граница длины задана математикой. Короткие ключи из рекламных буклетов живут в другой архитектуре: там строка — просто номер, который сервер сверяет со своей базой, и без интернета такой ключ не значит ничего. Я сознательно выбрал длинную строку и работу без сети вместо короткой строки и обязательного подключения. Так что 202 символа — это не «коротко», это наименее длинно из возможного.

Мелочь, которая ломает фиксированную длину

Уже по ходу дела выяснилась деталь, которую я по неопытности мог и пропустить. Подпись ECDSA записывают в двух видах, и один из них имеет плавающую длину: числа r и s кодируются как целые, а у них бывает разное количество значащих байт. Я подписал один и тот же блок данных пять тысяч раз.

Формат записи

Что получилось

Итог

DER, он же Rfc3279DerSequence

69 Б — 0,2 %, 70 Б — 25,3 %, 71 Б — 49,7 %, 72 Б — 24,9 %

длина плавает

IEEE P1363, он же r‖s

64 Б — 100 % случаев

длина фиксирована

Для формата, где длина ключа известна заранее и разбор идёт по смещениям, годится только второй. К счастью, в .NET именно он стоит по умолчанию: SignData без указания формата возвращает P1363. Если бы я машинально взял DER, как принято в мире OpenSSL, ключ получился бы переменной длины, а разбор оброс проверками на ровном месте.

Похожая история с публичным ключом, который вшивается в приложение. В полном виде, с описанием алгоритма и кривой, он занимает 91 байт и превращается в 124 символа Base64 (выбор на более компактном варианте, т.к. читаемость уже не нужна, ключ вшит). Но кривая у меня одна и известна заранее, поэтому я храню только сами координаты точки: 64 байта, то есть 88 символов. Мелочь, а константа в исходнике выглядит человечнее. Кстати, именно эти 88 символов однажды вставили в окно активации вместо лицензионного ключа — с тех пор программа отдельно объясняет, что публичный ключ и ключ лицензии это разные вещи.

Чем пришлось “заплатить”

Было бы нечестно закончить на том, какой я молодец. У выбранного варианта есть недостаток, и он не косметический.

ECDSA зависит от качества случайного числа. При каждой подписи генерируется одноразовое значение, и если оно повторится для двух разных сообщений или окажется предсказуемым, приватный ключ вычисляется из этих подписей. На этом в своё время сгорели вполне серьёзные системы. У Ed25519 такой проблемы нет по построению: там значение выводится детерминированно из ключа и сообщения по RFC 8032. Мой аргумент в свою защиту скромный: подписываю я на одной офлайн-машине штатным генератором операционной системы, а не на устройствах пользователей, так что поверхность у этого риска маленькая. Но риск есть, и делать вид, что его нет, я не буду.

Побочный эффект той же недетерминированности проще: если перевыпустить одну и ту же лицензию дважды, строки ключа получатся разные. Мелочь, но реестр выданных ключей это должен учитывать, иначе однажды удивишься.

И третье, о чём меня наверняка спросят в комментариях, так что скажу сам и сразу: к кривой P-256 у части криптографов есть вопросы по происхождению её констант, и Ed25519 в этом смысле репутационно чище. Для моей модели угроз — «подделать лицензию недорогой десктопной программы» — этой разницей можно пренебречь. Для чего-то серьёзнее я бы взвешивал решения заново.

Что в итоге

Я остался на ECDSA P-256 с SHA-256: подпись 64 байта, ключ 202 символа, проверка 0,19 мс, ноль сторонних зависимостей, кроссплатформенно из коробки — сервер, который у меня подписывает служебные подтверждения, работает на Linux тем же кодом. Пять вещей, которые я из этого вынес.

1. «Модно» и «доступно» — разные списки. Ed25519 стоит везде, где вы посмотрите, кроме стандартной библиотеки той платформы, на которой вы пишете. Проверять надо не общественное мнение, а свою сборку.

2. Критерий выбора диктует форма продукта, а не бенчмарк. У меня всё решила длина строки в письме: 202 символа против 571. Скорость подписи, вокруг которой обычно ломают копья, не повлияла ни на что.

3. Зависимость живёт своим графиком. Четыре мегабайта чужого кода — это не только четыре мегабайта: это ещё и чужие решения о том, какие версии платформы поддерживать.

4. Мерить, а не вспоминать. У меня были записаны замеры полугодовой давности, но проект, где они делались, я удалил. Пришлось перемерить всё заново — и одно число из старых записей оказалось не тем, чем я его помнил.

5. Проигрыш в бенчмарке — не повод менять решение. Мой алгоритм медленнее обоих конкурентов на проверке. На фоне 6,81 мс разовой инициализации это не значит ничего.

Спасибо, если дочитали.

Если у вас в проекте лежит криптография, выбранная «по умолчанию, все же так делают», — самое время открыть и проверить, что именно вы выбрали и почему.

Комментарии (10)


  1. vobinoy987
    28.07.2026 02:31

    200 символов диктовать по телефону)


    1. Vest
      28.07.2026 02:31

      Зачем, если есть SMS?


      1. BborlasS Автор
        28.07.2026 02:31

        Если речь про активацию в духе Windows XP: «позвоните, продиктуйте номер установки, получите код» то это не альтернатива подписи, а тот же самый механизм, только с человеком вместо интернета. Короткий код подтверждения в такой схеме ровно криптографический ответ сервера, просто усечённый до длины, которую можно продиктовать голосом; проверяет его всё равно программа на машине пользователя, всё той же математикой.

        Есть и практическая сторона. Мой ключ самодостаточен: в нём лежат данные лицензии и подпись, потому что программе больше неоткуда взять правду: ни базы, ни сети. 202 символа в SMS не влезают (70 знаков кириллицей, 160 латиницей), а диктовать их по телефону это пытка для обеих сторон )). Короткими коды у Microsoft были не от хорошей жизни и не от лучшей криптографии: там строка, не документ, а челлендж к их базе, то есть решение принимает сервер, а телефон просто канал связи. Я же специально делал вариант, который работает без канала связи вообще.

        Плюс два соображения, не технических. SMS - это платный шлюз, который должен жить круглосуточно и не падать, иначе покупатель не может включить то, за что заплатил. И это номер телефона, то есть персональные данные: у меня в базе сейчас хэш ключа и хэш почты с маской, ни одного телефона, и мне так спокойнее и юридически, и по-человечески.

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


    1. BborlasS Автор
      28.07.2026 02:31

      Фигура речи)) Конечно столько никто диктовать не будет ))


  1. erebmaethor
    28.07.2026 02:31

    А как реализована ротация ключей и защита от ввода одного ключа (J3QQ4-...) в несколько инстансов софтины?


    1. BborlasS Автор
      28.07.2026 02:31

      Сразу спойлер: эта тема по хорошему часть следующей статьи "продолжения", так как первая, текущая, описывала анализ и исследовательскую часть в варианте оффлайн, а вторая, ту которую я в итоге и допилил, уже построена на онлайн активации с участием сервера.

      Коротко ответить к сожелению наверное не выйдет.

      Ответ разделю условно на две части.

      Ротация. Первый байт полезной нагрузки - номер ключа подписи (keyId). В приложении вшит не один публичный ключ, а словарь «номер → ключ». Смена выглядит так: генерирую новую пару, добавляю в словарь строку с новым номером, новые лицензии подписываю ею. Ранее выданные продолжают проверяться старым публичным ключом — ротация не должна "наказывать" тех, кто уже заплатил.

      Если утёк приватный ключ, одной ротации мало: вор печатает валидные лицензии на старом. План на этот случай - релиз с новым keyId, старый помечается устаревшим, и проверка для него переводится в режим «доверяем только номерам лицензий из белого списка». Это работает потому, что у каждой лицензии есть собственный номер (4 байта в payload) и локальный реестр выданных: ключ, напечатанный вором, будет с номером, которого в реестре нет. Есть конечно и ограничение - и новый keyId, и список отзыва доезжают только до тех, кто обновился. Автообновление есть, но мгновенным это не делает.

      Вторая пара ключей - у сервера активации, он подписывает короткоживущие тикеты, номер ключа едет прямо в тикете. Там ротация проще: тикет живёт 30 дней, обратная совместимость не нужна, достаточно, чтобы клиент при «подпись не сошлась» молча пошёл активироваться заново. Ровно эту ветку я сначала и не написал, а ключ сервера пришлось менять внепланово. На тесте вылезло: клиент со старым тикетом получал невалидную подпись и тихо уезжал в бесплатный режим вместо переактивации, то есть при смене ключа платный уровень погас бы разом у всех.

      Два урока оттуда:

      • ротация не считается сделанной, пока не прогнал клиента, у которого на руках СТАРЫЙ артефакт;

      • порядок проверки «сначала ключ, потом тикет» пришлось перевернуть - негодная доверенность не должна мешать активации.

      Один ключ в нескольких инстансах. Сама подписанная строка этого не умеет в принципе: подпись доказывает подлинность, а не уникальность. Счётчик активаций внутрь ключа не положить - payload скреплён подписью, а переподписать его на машине пользователя нечем, отдельный локальный счётчик рядом обнуляется копированием файла.

      Единственное место, где такой счётчик может жить - это сервер.

      Поэтому поверх офлайновой подписи стоит онлайн активация:

      • клиент шлёт ключ и обезличенный отпечаток машины — HMAC-SHA256 от MachineGuid и серийника системного тома, первые 16 байт;

      • сервер занимает слот (в рознице seats = 1) и возвращает подписанный СВОИМ ключом тикет: номер лицензии, хэш ключа, отпечаток устройства, слот, срок 30 дней, nonce клиента;

      • дальше программа работает офлайн: проверяет подпись сервера, пересчитывает отпечаток, смотрит срок. За подтверждением ходит не чаще раза в сутки, без сети живёт до 30 дней.

      Второй экземпляр того же ключа получает отказ SEAT_TAKEN. Скопировать другу ключ вместе с тикетом тоже не помогает: доверенность выписана на чужое железо, отпечаток не сойдётся, а новую сервер не выдаст - слот занят. Переустановка Windows, поломка, переезд решаются не криптографией, а кнопкой «Освободить и активировать здесь» (доказательство владения - предъявить полный ключ), не чаще трёх раз в 30 дней, чтобы это не превратилось в «пользуемся вдвоём по очереди».

      В базе при этом ни ключей, ни адресов в открытом виде: хэш ключа, хэш почты с маской, хэш отпечатка - утечка базы не даёт ни одной работающей лицензии.

      И сразу предел всего происходящего, чтобы не создавать ложного впечатления: всё это защищает от копирования ключа, но не от патча бинарника. С ним конечно все обстоит куда сложнее.


  1. fedorro
    28.07.2026 02:31

    Какой-то оверкил для недорогой десктопной программы.

    подпись перепроверяется в каждой точке, где платная функция реально исполняется

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


    1. BborlasS Автор
      28.07.2026 02:31

      Согласен с вами, я в статье сформулировал слишком широко. Перепроверки спасают от класса «правка живого значения»: единый флаг «оплачено» это поле, которое живёт весь сеанс, его находят поиском в памяти и переключают, не пересобирая сборку и не читая код. Но это время, желание сломать и навыки с знаниями. Когда решение нигде не залипает, переключать нечего.

      От правки самой сборки они не спасают - тут вы правы. Гейт это один метод, и патчится он один раз, сколько бы точек его ни звали. Мне наверное конечно стоит подправить свою формулировку: не «защищает от патча», а «поднимает атаку на ступеньку: с правки значения в памяти до правки кода». Ступенька, не стена, в статье стоило написать именно так.

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

      Про оверкил: крипта здесь стоила пары сотен строк на встроенной библиотеке и 0,19 мс на проверку. Настоящим оверкилом было бы городить обфускацию до первых реальных продаж - этот шаг я как раз отложил.

      Про ИИ, который всё автоматизирует - согласен. Гонку «сделать невзламываемо» недорогая десктопная программа выигрывать не должна и не может. Задача скромнее: чтобы честный путь был проще нечестного для обычного человека.


  1. Granulex
    28.07.2026 02:31

    Про 571-символьную "простыню" от RSA – прямо больно и знакомо. Есть ирония уровнем выше: вся эта аккуратная криптография честно защищает от того, чего никто не делает. Ключ никто не станет подделывать – его просто купят один раз и выложат на форум, а без сервера программа будет с гордостью проверять валиднейшую подпись на тысяче чужих машин. Математика подписи безупречна ровно до того момента, как один довольный пользователь нажмёт Ctrl+C.


    1. BborlasS Автор
      28.07.2026 02:31

      Здесь аналогичная тема ложащаяся в планируемое продолжение, где я бы раскрыл онлайн активацию и механизмы контроля, в текущей статье был условный первый этап, через который я сознательно решил пройти, так как любую тему пытаюсь пройти от "фундамента". Данные задачи я ставлю перед собой и разбираю в первую очередь для самообучения, так проще когда есть прикладная задача.

      Согласен с вами по фактам и не согласен с выводом — попробую объяснить свою позицию.

      Да, подпись не защищает от Ctrl+C, и не должна: её работа - доказать, что документ выписал я и что байты не переписали по дороге. «Сколько человек пользуется одним документом» это вопрос учёта, и в оффлайновой схеме, согласен, он не решается никак: счётчик внутрь ключа не положить (payload скреплён подписью, переподписать на машине пользователя нечем), а счётчик в файле рядом обнуляется копированием этого файла.

      Причём описанный сценарий у меня не гипотетический. Это ровно первая редакция проекта: чистый офлайн, ключ из письма, никакого сервера. Я её зафиксировал и продолжил развивать примерно по вашим же аргументам, а статья описывает выбор алгоритма внутри слоя «транспорт прав», а не всю защиту целиком. J3QQ4-… из соседнего комментария жив как раз потому, что в его эпоху отклонять было нечему.

      Что стоит поверх: активация на сервере. Ключ плюс обезличенный отпечаток машины: сервер занимает единственный слот и выдаёт подписанную им доверенность на 30 дней, привязанную к этому отпечатку. Выложенный на форум ключ активируется у первого пришедшего, все остальные получают «слот занят». Копирование ключа вместе с доверенностью не помогает: она выписана на чужое железо.

      А теперь ирония в обратную сторону, ради которой я и отвечаю. Как только появляется сервер, криптография не становится лишней - она становится сильно актуальнее. Онлайн проверка без подписанных ответов ломается за десять минут: строка в hosts, свой «сервер активации» на localhost, свой сертификат и программа радостно слышат «лицензия подтверждена». HTTPS здесь не спасает: сертификат для собственного локального сервера пользователь на своей машине выпишет сам при должных знаниях и желании. Спасает то, что ответ подписан отдельным серверным ключом, публичная часть которого вшита в программу, плюс nonce от клиента, чтобы нельзя было подсунуть записанный вчера ответ. Подделать это - значит подделать подпись, то есть ровно та задача, про которую статья.

      И вторая работа для той же подписи: она позволяет не ходить в сеть. Доверенность самоподписана сервером, поэтому программа спокойно живёт офлайн до 30 дней вместо безуспешного стука на сервер при каждом запуске.

      Против: «вырезал проверку из бинарника» не работает ни подпись, ни сервер - только удорожание взлома, и я не топлю про 100% защиту. Задача этого слоя скромнее: закрыть массовый бытовой канал «скинь ключик», а он и составляет практически все потери у продуктов такого размера.