Криптография на Rust и немного FFI
- вторник, 18 июля 2023 г. в 00:00:19
Частичный перевод моей статьи с Medium
Начнем с того, что эта статья как и большинство моих статей родилась в процессе разработки. В данном конкретном случае - разработки внешней крипто-библиотеки для генерации крипто-кошельков.
Да, определенно. Я считаю, что приставка "крипто" любому словосочетанию придает привкус гигантской яхты дрейфующей где-то в Майами.
В итоге, выбор упал на Rust потому что я уже работал с ним, и знаю, что crates.io изобилует всякими библиотеками для работы с разнообразными криптографическими функциями.
Всеми признанный король криптовалют использует функцию 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.
Дальнейшей целью было портирование данной библиотеки в 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
Внимание! Спасибо за внимание. Надеюсь мой опыт окажется кому-то полезен, и еще надеюсь, что я ничего не упустил и не перепутал :)
Мой тг: