golang

Криптография на Rust и немного FFI

  • вторник, 18 июля 2023 г. в 00:00:19
https://habr.com/ru/articles/748534/

Частичный перевод моей статьи с Medium

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

Да, определенно. Я считаю, что приставка "крипто" любому словосочетанию придает привкус гигантской яхты дрейфующей где-то в Майами.

В итоге, выбор упал на Rust потому что я уже работал с ним, и знаю, что crates.io изобилует всякими библиотеками для работы с разнообразными криптографическими функциями.

Bitcoin

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

Ключ-пара (Keypair) состоит из двух частей - приватный ключ и публичный, который выводится из приватного путем "Деривации"

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

Обычно приватный ключ - это просто случайное 256-битное число между 0 и n - 1, где n - константа (n = 1.1578 * 10⁷⁷). Как я уже сказал в Bitcoin публичный ключ выводится из приватного при помощи функции secp256k1 которая имеет 128-битную сложность. Это значит, что для того, чтобы провести обратную деривацию потребуется ~2¹²⁸ операций (тут могло бы быть сравнение с кол-вом атомов во вселенной). Короче, вся безопасность вашего приватного ключа основывается на том, насколько сложно получить его из публичного.

Обратно никак
Обратно никак

Больше технических подробностей про сложность и входные параметры secp256k1 любителям технических подробностей (стр. 4 и cтр. 9).

Вот пример того как сгенерировать пару ключей для Bitcoin используя крейт secp256k1. OsRng тут - это структура-генератор "Достаточно рандомных чисел" из крейта rand.

Окей, у нас есть пара ключей, So what? Чтобы мы могли принимать Bitcoin от кого-то, нам нужно конвертировать публичный ключ в Адрес.

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

Чтобы получить адрес из публичного ключа нам нужно прогнать его через double-hash функцию (SHA256 + RIPEMP160, иногда еще называют HASH160) и потом кодировать результат используя Base58Check.

Компутируем HASH160, должен быть 20 байтов в длину. В ширину не важно, вроде как

Интересный момент, Base58Check используется не случайно. Эта кодировка предоставляет дополнительные проверки на опечатки, а именно Байт версии и Чексумму.

Байт версии (Version byte) - индикатор сети (0x00 в mainnet например, список всех возможных префиксов)

Чексумма (Checksum) - первые 4 байта результата конкатенации байта версии и SHA256(SHA256(HASH160))

Картинки я сам рисовал
Картинки я сам рисовал

Инсертим байт версии в начало вектора с HASH160. Два раза через SHA256 и достаем первые 4 байта, достаточно просто.

Следущий шаг - конкатенировать HASH160 с чексуммой и прогнать это все через base58 используя крейт bs58.

Теперь у нас есть случайно сгенерированный приватный ключ, публичный ключ полученный из него и Bitcoin адрес, на который мы уже можем принимать бинкоины чинаа. Последний шаг - конвертировать приватный ключ в WIF (Wallet Import Format). Для этого нужно просто прогнать его через Base58Check с байтом версии 0x80.

Ура! Мы создали Bitcoin кошелек с WIF приватным ключом и Base58 адресом.


В оригинальной статье я дополнительно рассказываю про генерацию кошельков под Ethereum, Litecoin, Solana, Tron, Aptos и Sui и немного про Ethereum Keystore для безопасного хранения приватных ключей. Но тут я в заголовке пообещал FFI, поэтому придется про FFI.

FFI (Foreign Function Interface)

Дальнейшей целью было портирование данной библиотеки в Golang (grpc не предлагать). Поэтому я решил просто сбилдить динамическую библиотеку (.dylib) и использовать ее через CGO.

Для этого надо прописать в Cargo.toml, что мы билдим библиотеку как cdylib

Напишем небольшой библиотечный крейт с функцией generate_wallet, которая принимает enum Network с доступными сетями. Атрибутный макрос #[no_mangle] говорит компилятору не изменять имя функции во время компиляции (это называется Name mangling). Из функции мы возвращаем сырой указатель на нуль-терминированную строку. Атрибутный Макрос #[repr(C)] значит, что enum будет расположен в памяти как в C.

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

Теперь просто импортируем нашу библиотеку вместе с заголовочным файлом через преамбулу CGO. С.GoString мы используем, чтобы привести указатель char* к string. Go итерируется по памяти пока не встретит нуль терминатор \0.

  • -L - По умолчанию .dylib файл сохраняется в target/debug/

  • -l - По умолчанию соответствует имени из Cargo.toml

Ну вот и все, теперь билдим либу cargo build. И возможно если Go изначально не был настроен (как у меня) надо будет добавить несколько env переменных указывающих на архитектуру (список всех возможных архитектур и осей) CGO_ENABLED="1" GOOS="darwin" GOARCH="arm64" go build


Внимание! Спасибо за внимание. Надеюсь мой опыт окажется кому-то полезен, и еще надеюсь, что я ничего не упустил и не перепутал :)

Мой тг: