/
Поиск
RU

Криптография в std

std/crypto — самая большая часть стандартной библиотеки kola: больше сорока модулей, в которых есть SHA-2 и SHA-3, HMAC, HKDF, PBKDF2 и Argon2, AES-GCM и ChaCha20-Poly1305, X25519, Ed25519, P-256 и P-384, RSA, постквантовые схемы NIST, разбор и выпуск сертификатов X.509 и выработка ключей TLS 1.3. Всё это встроено в бинарник компилятора и подключается одной строкой use.

Эта глава — карта и свод правил, а не справочник по API: точные сигнатуры записаны в заголовочном комментарии каждого модуля (std/crypto/<name>.kl), а kola help std-index перечисляет все публичные элементы. Всё, о чём пойдёт речь дальше, определяет один факт. Функция в kola объявляет свои эффекты в сигнатуре, и каждая функция в std/crypto объявлена либо как !{} (чистое вычисление), либо как !{alloc(r)} (вычисление, которое ещё и выделяет память). Ни одна из них не выполняет io — эффект, которым kola помечает обращения к файлам, сокетам и часам. Хеш-функция не может получить ни одной капабилити, поэтому криптографическая ошибка в этом дереве не может заодно стать каналом утечки: этому коду просто не с кем связаться. По той же причине все примеры здесь — обычные test без Sys.

Что есть в дереве

Хеш-функции

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

  • sha256, sha384, sha512: FIPS 180-4. У каждого есть однократный digest(&msg), который возвращает массив фиксированного размера, а у sha1 и sha256 — ещё и потоковый интерфейс init/update/finish для сообщения, которое никогда не лежит в памяти целиком.

  • keccak и fips202: SHA-3 и SHAKE. keccak — удобный фасад (sha3_256, sha3_512, shake256_32), а fips202 — механизм губки под ним (впитывание и отжим, absorb/squeeze), которым постквантовые модули пользуются напрямую.

  • blake2s и blake2b: RFC 7693. blake2s нужен WireGuard (хеш без ключа и 128-битный MAC с ключом), blake2b — Argon2, которому требуется длина результата, выбираемая во время выполнения.

  • sha1: только для совместимости. Его стойкость к коллизиям взломана, а в библиотеке он потому, что идентификаторы объектов Git, старые транскрипты TLS и HMAC-SHA-1 по-прежнему встречаются на практике.

  • md5: криптографически взломан, коллизии с выбранным префиксом строятся на практике. Он есть ради форматов обмена, которые его требуют, например устаревшей парольной аутентификации md5 в PostgreSQL. Никогда не используйте его для контроля целостности и вообще там, где выбор за вами.

MAC и выработка ключей

MAC (message authentication code, код аутентификации сообщения) — короткий тег, вычисленный по сообщению и секретному ключу: тот, кто знает ключ, может проверить, что сообщение не изменили. Функция выработки ключей (KDF) растягивает один секрет или делит его на те ключи, которые нужны протоколу. KDF на основе пароля нарочно медленная, чтобы перебор паролей обходился дорого.

  • hmac: RFC 2104 поверх SHA-1, SHA-256, SHA-384, SHA-512 и BLAKE2s. У каждой hmac_* есть двойник hmac_*_verify, который сравнивает тег за константное время. Модуль ничего не выделяет: внутренняя конкатенация проходит через потоковое состояние хеша.

  • hkdf: извлечение и расширение (extract и expand) по RFC 5869. hkdf_expand_* пишет в &mut [u8], которым владеет вызывающий, поэтому результату, длина которого известна только во время выполнения, регион не нужен.

  • pbkdf2 и argon2: выработка ключа из пароля. В Argon2 есть argon2d, argon2i и argon2id (RFC 9106); выбирайте его, если протокол не предписывает PBKDF2.

  • poly1305: одноразовый MAC (RFC 8439). Пользуйтесь AEAD ChaCha20-Poly1305: он сам выводит новый ключ для каждого nonce.

  • scram: клиентская половина SCRAM-SHA-256 (RFC 5802 и 7677) — механизма SASL, которым пользуются PostgreSQL, MongoDB, Kafka и другие. Клиентский nonce передаётся параметром, поэтому весь обмен — чистая функция, и его можно проверить по тестовому вектору из RFC.

Шифры и AEAD

Шифр зашифровывает данные ключом. На практике почти всегда нужен не голый шифр, а шифрование с аутентификацией, которое ещё и обнаруживает подделку. Каждое шифрование принимает также nonce — значение, которое никогда не должно повторяться с одним и тем же ключом.

  • aes: развёртывание ключа и шифрование одного блока для AES-128 и AES-256, целиком на ct fn. aes_ctr добавляет режим счётчика, а aes_xts — посекторный режим XTS для блочных хранилищ.

  • aes_gcm: AEAD для обоих размеров ключа: пара операций seal (зашифровать и выдать тег) и open (проверить тег и расшифровать). У каждой есть вариант _rk, который принимает заранее развёрнутый ключ и ключ GHASH, чтобы долгая сессия не разворачивала ключ заново для каждой записи.

  • chacha20: сам потоковый шифр, AEAD по RFC 8439 (chacha20poly1305_seal/_open) и XChaCha20-Poly1305 с 24-байтовым nonce. salsa20 — его предшественник, оставленный для форматов, которые его требуют.

Кривые, обмен ключами, подписи

Криптография с открытым ключом работает с парой ключей: закрытый вы храните у себя, открытый публикуете. Обмен ключами (X25519, ECDH) позволяет двум сторонам вывести общий секрет из открытых ключей друг друга, а подпись (Ed25519, ECDSA, RSA) доказывает, что владелец закрытого ключа одобрил сообщение.

  • x25519: RFC 7748, ct от начала до конца. x25519(&Secret[[u8; 32]], pub u) возвращает Option, и None в нём означает результат малого порядка; pubkey_gen отказать не может.

  • ed25519: public_key, sign и verify с учётом кофактора по RFC 8032. Зерно приходит упакованным, как &Secret[[u8; 32]]. sign_in и verify_in принимают рабочий Vec, которым владеет вызывающий, — для сервисов, которым не нужны выделения памяти в установившемся режиме.

  • p256 и p384: арифметика точек, ecdh и pubkey_gen, всё на ct.

  • ecdsa256: verify и sign; подпись берёт детерминированный nonce по RFC 6979, поэтому Rng ей не нужен. ecdsa384 умеет только проверять. Обе проверки намеренно написаны не на ct: все значения, с которыми они работают, публичны.

  • rsa: подпись и проверка PKCS#1 v1.5, подпись и проверка PSS, шифрование и расшифровка OAEP, всё поверх движка mont.

Постквантовые схемы

Большой квантовый компьютер взломал бы сегодняшние схемы на эллиптических кривых и RSA. Постквантовые схемы, которые NIST стандартизовал в 2024 году, рассчитаны на то, чтобы устоять и перед ним.

Это mlkem (ML-KEM-768), рядом с которым есть mlkem512 и mlkem1024, и mldsa (ML-DSA-65), в формах FIPS 203 и FIPS 204: keygen/encaps/decaps для механизма инкапсуляции ключа (KEM) и keygen_internal/sign_internal/verify_internal для подписи. Обе схемы проверяются по опубликованным трассам промежуточных значений, а не только по конечным результатам, так что дефект в середине вычисления не спрячется за правдоподобным ответом.

Сертификаты и TLS 1.3

Сертификат X.509 связывает открытый ключ с именем (например, с именем хоста) и подписан издателем; цепочка сертификатов ведёт назад, к издателю, которому вы уже доверяете.

  • x509: разбор DER-сертификата в Cert[r], живущий в регионе, а затем verify_chain, check_validity, hostname_matches и ip_matches. Поддерживаются алгоритмы подписи ECDSA с SHA-256, sha256WithRSAEncryption и Ed25519.

  • x509_build: выпуск сертификатов через build_self_signed_root и build_leaf и отдельные функции записи расширений. build_leaf принимает открытые координаты субъекта, так что закрытый ключ субъекта в модуль никогда не попадает.

  • pkcs10: разбор запроса на подпись сертификата и проверка его самоподписи.

  • tls13_keys и tls13_record: выработка ключей по RFC 8446 (HKDF-Expand-Label, Derive-Secret, хеш транскрипта, все секреты трафика) и seal/open на уровне записей для обоих наборов шифров. Это арифметика TLS, а конечный автомат рукопожатия живёт в пакете.

Движки

fe25519/sc25519, fe256/sc256, fe384/sc384 и mont — арифметика полей и скаляров и возведение в степень по Монтгомери с фиксированной разрядностью. Импортировать их можно, но это слои с жёсткими контрактами, а не интерфейсы протоколов. Стройте на модулях схем; если вам понадобился движок напрямую, то, что вы пишете, скорее всего, должно жить в самой std.

Правила безопасного использования

Константное время доказывается, а не обещается

Если время работы функции зависит от секрета, атакующий, который может её засекать, пусть даже по сети, узнаёт секрет бит за битом. Классический пример — побайтовое сравнение с досрочным выходом: оно отвечает тем раньше, чем раньше стоит первый неверный байт. Код константного времени работает одно и то же время и обращается к одним и тем же адресам памяти, каковы бы ни были значения секретов. ct fn проверяется именно на это: каждый её параметр считается секретом, и чекер отвергает всё, из-за чего время могло бы от него зависеть: ветвление, индексацию, деление, арифметику с трапом, вызов функции без ct. В этом дереве так написаны aes, x25519, арифметика точек p256/p384 и скалярная лестница Ed25519. Как это устроено, включая идиому выбора без ветвления и параметры pub, рассказано в главе Константное время (ct).

Теги, дайджесты и аутентификаторы сравнивайте через hmac::ct_eq(a, b) (или Secret::eq для секретов в коробке). Встроенный в язык == константного времени не гарантирует.

Недоверенная длина — не предусловие

Параметр можно пометить untrusted и тем самым объявить, что его байты приходят от атакующего: из сокета, файла, стандартного ввода. Тогда чекер отвергает любой контракт, который от этого параметра зависит. Причина в том, что контракт — это утверждение о вызывающем, а у байтов из сети вызывающего нет. Контракты соблюдаются всегда, поэтому req packet.len >= 32 на полезной нагрузке UDP означает, что на одном специально собранном 20-байтовом пакете сработает трап и процесс завершится, а маршрутизатор во freestanding-сборке (без операционной системы) остановится целиком. Небрежности в таком req нет: записать предусловие — нормальный рефлекс. Просто обещать его здесь некому.

// Заголовок кадра из сети: длина в 2 байта, затем тело.
pub fn frame_len(untrusted packet: &[u8]) -> u16 !{}
    req packet.len >= 2
{
    (widen[u16](packet[0]) << 8) | widen[u16](packet[1])
}
error[card §8]: `req` depends on untrusted input `packet`
  --> u.kl:2:9
   |
 2 |     req packet.len >= 2
   |         ^^^^^^^^^^^^^^^
   = help: this `req` depends on untrusted input `packet`; return `Result`/`Option` and check at runtime, or remove the `untrusted` marker if this param is not attacker-controlled

Исправление сохраняет проверку, но отказ становится значением, а не трапом:

pub fn frame_len(untrusted packet: &[u8]) -> Option[u16] !{} {
    if packet.len < 2 { return None; }
    Some((widen[u16](packet[0]) << 8) | widen[u16](packet[1]))
}

test short_packet_is_refused_not_fatal {
    let short: [u8; 1] = [0x01u8];
    assert frame_len(&short) == None;

    let full: [u8; 3] = [0x01u8, 0x2cu8, 0xffu8];
    assert frame_len(&full) == Some(300u16);
}

std и сама следует этому правилу. Функции вскрытия TLS-записей объявлены как open_record_aes128(key, static_iv, seq, untrusted body: &[u8], inner_out: &mut [u8]) -> bool, и испорченная запись даёт тот же false, что и неверный тег аутентификации. RFC 8446 считает обе ситуации одинаково фатальными, так что вызывающие всё равно обрабатывают их одинаково.

Secret[T]: где используется коробка

Secret[T] — встроенная непрозрачная линейная коробка. Линейная значит, что компилятор требует израсходовать каждую коробку ровно один раз на каждом пути, так что ключ нельзя молча забыть в памяти. Коробку нельзя превратить в строку (подстановка в f"..." — ошибка компиляции), её нужно израсходовать на каждом пути, а прочитать её можно только через Secret::expose, и это место легко найти grep'ом. Secret::wipe(s) расходует коробку и перезаписывает её байты. Это сложнее, чем кажется: оптимизирующий компилятор C видит, что обнулённую память больше никто не читает, удаляет цикл как мёртвую запись, и ключ остаётся в памяти. wipe пользуется volatile-записью, которую компилятор обязан сохранить. Подробности — в главе Константное время (ct).

Все точки входа, принимающие долгоживущий ключ, принимают коробку: ed25519::public_key и sign, x25519::x25519 и pubkey_gen, p256::ecdh, p256::pubkey_gen и p384::ecdh, ecdsa256::sign, keygen и decaps в ML-KEM, sign_internal в ML-DSA и ключ подписи в x509_build. Функции AEAD намеренно принимают обычные массивы (key: [u8; 16] в aes_gcm, key: [u8; 32] в chacha20, PRK в HKDF): ключ записи участвует в обработке каждого пакета, и коробка добавила бы по expose на каждую запись. Поэтому вызывающий, у которого ключ лежит в коробке, раскрывает его один раз, на границе AEAD. Раскрывайте как можно позже, используйте сразу и дайте раскрытой копии умереть в самой узкой области видимости, какую удастся устроить.

Секрет, длина которого известна только во время выполнения, кладут в Secret[Vec[u8]]. Secret::wrap(v) перемещает Vec внутрь, expose заимствует его байты, а wipe затирает всю ёмкость буфера. Чтобы обрабатывать поток таких секретов, не раздувая кучу, есть Secret::wipe_into(s): он затирает коробку и возвращает тот же буфер, пустым, для повторного использования. Упаковать нельзя только ссылку: Secret[&r [u8]] отвергается, потому что, затерев указатель, мы оставили бы нетронутым открытый текст за ним. Байты, которые вам только одолжили, например буфер запроса, затирают на месте через Secret::wipe_bytes. А значение, которое приходится выводить как текст, например bearer-токен в сетевом сообщении, вообще не случай для Secret: для него правило — сравнение за константное время и wipe_bytes в конце жизни.

Случайность: граница проходит по предсказуемости

Случайные числа бывают двух видов. Генератор псевдослучайных чисел вычисляет длинную последовательность из маленького зерна: быстро и воспроизводимо, но тот, кто узнал его внутреннее состояние, может вычислить все последующие значения. Источник энтропии операционной системы (getrandom в Linux) собирает непредсказуемость из аппаратуры, и ключи должны браться именно оттуда. Rng::weak_u64(&mut rng) -> u64 относится к первому виду: это генератор xorshift64*, который один раз получает зерно из 8 байтов энтропии ОС (или из --seed). О нём легко упустить два факта. Каждое значение, которое программа из него берёт, происходит из этих 64 бит, так что целый набор ключей, выпущенных из него, несёт в сумме 64 бита энтропии. И его состояние восстанавливается по одному выходному значению: одно подсмотренное значение предсказывает все последующие. Поэтому nonce протокола, ClientHello.random или идентификатор запроса, взятые из него, выдают наблюдателю закрытый ключ, который вы возьмёте следующим.

Критерий здесь не в том, сколько живёт значение, а в том, выиграет ли кто-нибудь, если его предскажет. Каждому случайному значению задайте два вопроса: узнает ли что-нибудь противник, который его увидит, и ослабнет ли оно, если вторая установка получит ту же последовательность? Первый вопрос отсеивает ключи, токены и эфемерные секреты. Второй — значения, которые не секретны, но должны быть уникальны для каждой установки, например соль PBKDF2 или идентификатор в базе данных. Rng::weak_u64 годится для того, что проходит оба: для случайного разброса задержек, перемешивания, распределения нагрузки и тестовых данных.

Всё остальное при каждом вызове берётся у ОС, и происхождение значения записано в его типе. Такие значения выпускает Entropy::draw[[u8; N]](&mut rng) -> Result[Entropy[[u8; N]], (IoErr, OsErr)] !{io}, а Entropy::expose(&e) — единственный способ их прочитать. Entropy фиксирует, откуда взялись байты; Secret говорит, что читать их без нужды нельзя, а поскольку Secret::wrap публичен, о происхождении он ничего не утверждает. Одно сочетается с другим, и ключу нужно именно это: Secret::wrap(Entropy::expose(&e)). Для буфера, длина которого известна только во время выполнения, rng.fill_os(&mut buf) берёт байты из того же источника. rng.split() второго источника не даёт: дочерняя последовательность получает зерно из одного значения родительской и энтропии не добавляет, поэтому рабочий поток, которому нужен ключевой материал, сам вызывает Entropy::draw.

Эти вызовы — io, и freestanding-сборка их отвергает: там нет ОС, у которой можно спросить. Под --seed и kola test они намеренно переходят на последовательность от зерна, чтобы прогоны повторялись в точности; полученные так ключи — тестовые. Entropy::of(x) упаковывает фиксированный массив для тестового вектора из RFC; kola check принимает такой вызов где угодно, а kola audit отвергает на любом пути, который начинается не в элементе test.

use std::crypto::ed25519;
use std::stdio;

// Ключ, который будут хранить: берём его как Entropy, затем кладём в Secret.
fn main(mut sys: Sys) -> u8 !{io} {
    let seed = match Entropy::draw[[u8; 32]](&mut sys.rng) {
        Ok(e) => e,
        Err(_) => { return 1; }
    };
    let sk = Secret::wrap(Entropy::expose(&seed));
    let public = ed25519::public_key(&sk);
    Secret::wipe(sk);
    let _ = stdio::write_all(&sys.stdout, &public);
    0
}

kola run keygen.kl каждый раз печатает другой открытый ключ, а kola run --seed 7 keygen.kl — всякий раз один и тот же, и именно поэтому упавший тест, работающий с ключами, воспроизводим. Два отчёта проверяют обратное направление. kola report secrets находит константу времени компиляции, которая попадает в позицию типа Secret (учётные данные, зашитые в бинарник), а kola report wipe-windows проверяет место за местом, ограничено ли каждое затирание через Secret::wipe_bytes, то есть можно ли указать, сколько байтов оно затронет.

Проверяйте по байтам, вычисленным вне kola

Каждый модуль проверяется тестами с известным ответом, то есть опубликованными парами входов и выходов из стандарта, который его определяет: FIPS 180-4 для SHA-2, RFC 4231 для HMAC, RFC 5869 для HKDF, RFC 8032 для Ed25519, NIST CAVP для P-256, NIST ACVP для постквантовых схем. Ожидаемые байты вычисляются вне kola, с помощью openssl или независимого переложения спецификации, и записываются раньше, чем код на kola.

Дело в том, что тест «туда и обратно» не видит дефектов канонической формы. Представьте кодировщик DER, который всегда записывает в BIT STRING ноль в байт неиспользуемых битов. Для подписи это правильно, а для списка именованных битов — нет, так что каждое расширение keyUsage, которое он выпускает, неканонично: 03 02 00 80 там, где другие издатели пишут 03 02 07 80. Сертификаты при этом проходят проверку, собственный парсер модуля читает их обратно, и тест, который выпускает, разбирает и проверяет сертификат, остаётся зелёным: кодировщик и парсер, которые заблуждаются одинаково, друг с другом согласны. Поймать такое могут только ожидаемые байты от независимого кодировщика.

Три разобранных примера

Все три — блоки test без капабилити, потому что ничто в std::crypto их не требует. Каждый проходит под kola test в обоих исполнителях.

Хеш, затем MAC, затем выработка ключа

Цепочка, с которой начинается большинство протоколов, проверенная по тестовым векторам трёх стандартов.

use std::crypto::sha256;
use std::crypto::hmac;
use std::crypto::hkdf;

test hash_mac_kdf_chain {
    // FIPS 180-4: SHA-256("abc")
    assert sha256::digest(&b"abc") == [
        0xbau8, 0x78u8, 0x16u8, 0xbfu8, 0x8fu8, 0x01u8, 0xcfu8, 0xeau8,
        0x41u8, 0x41u8, 0x40u8, 0xdeu8, 0x5du8, 0xaeu8, 0x22u8, 0x23u8,
        0xb0u8, 0x03u8, 0x61u8, 0xa3u8, 0x96u8, 0x17u8, 0x7au8, 0x9cu8,
        0xb4u8, 0x10u8, 0xffu8, 0x61u8, 0xf2u8, 0x00u8, 0x15u8, 0xadu8,
    ];

    // RFC 4231, случай 1: key = 20 x 0x0b, msg = "Hi There"
    let key: [u8; 20] = [0x0bu8; 20];
    assert hmac::hmac_sha256(&key, &b"Hi There") == [
        0xb0u8, 0x34u8, 0x4cu8, 0x61u8, 0xd8u8, 0xdbu8, 0x38u8, 0x53u8,
        0x5cu8, 0xa8u8, 0xafu8, 0xceu8, 0xafu8, 0x0bu8, 0xf1u8, 0x2bu8,
        0x88u8, 0x1du8, 0xc2u8, 0x00u8, 0xc9u8, 0x83u8, 0x3du8, 0xa7u8,
        0x26u8, 0xe9u8, 0x37u8, 0x6cu8, 0x2eu8, 0x32u8, 0xcfu8, 0xf7u8,
    ];

    // RFC 5869 A.1: извлечь PRK, затем развернуть его в буфер вызывающего
    let ikm: [u8; 22] = [0x0bu8; 22];
    let salt = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c";
    let prk = hkdf::hkdf_extract_sha256(&salt, &ikm);
    let mut okm: [u8; 16] = [0u8; 16];
    hkdf::hkdf_expand_sha256(prk, &b"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", &mut okm);
    assert okm == [
        0x3cu8, 0xb2u8, 0x5fu8, 0x25u8, 0xfau8, 0xacu8, 0xd5u8, 0x7au8,
        0x90u8, 0x43u8, 0x4fu8, 0x64u8, 0xd0u8, 0x36u8, 0x2fu8, 0x2au8,
    ];
}

AEAD: seal, open и испорченный байт

Буфер для результата — ваш, и его длина задана контрактом (req out.len == plaintext.len): AEAD здесь ничего не перевыделяет. open возвращает bool, а не отдаёт неаутентифицированный открытый текст в надежде, что вы его проверите; при неверном теге буфер назначения остаётся обнулённым.

use std::crypto::chacha20;

test aead_seal_open {
    let key: [u8; 32] = [0x42u8; 32];
    let nonce: [u8; 12] = [0x24u8; 12];

    let mut ct: [u8; 14] = [0u8; 14];
    let tag = chacha20::chacha20poly1305_seal(key, nonce, &b"hdr", &b"attack at dawn", &mut ct);

    // Ожидаемые байты вычислены вне kola (пакет Python `cryptography`),
    // а не расшифровкой нашего же шифртекста.
    assert ct == [
        0x85u8, 0x73u8, 0xf1u8, 0x6fu8, 0x88u8, 0xb5u8, 0x78u8, 0xc9u8,
        0xf1u8, 0x4bu8, 0xecu8, 0xeau8, 0x93u8, 0x79u8,
    ];
    assert tag == [
        0x94u8, 0x26u8, 0x14u8, 0x4au8, 0xb6u8, 0xd8u8, 0x74u8, 0x02u8,
        0x22u8, 0x4cu8, 0x7fu8, 0x43u8, 0x04u8, 0xa8u8, 0x12u8, 0xf4u8,
    ];

    let mut pt: [u8; 14] = [0u8; 14];
    let ok = chacha20::chacha20poly1305_open(key, nonce, &b"hdr", &ct, tag, &mut pt);
    assert ok;
    assert pt == b"attack at dawn";

    // Портим один байт шифртекста: open возвращает false и не пишет открытый текст.
    let mut bad = ct;
    bad[0] = bad[0] ^ 0x01u8;
    let mut pt2: [u8; 14] = [0u8; 14];
    let ok2 = chacha20::chacha20poly1305_open(key, nonce, &b"hdr", &bad, tag, &mut pt2);
    assert !ok2;
}

Ed25519: вывести ключ, подписать, проверить и отвергнуть подделку

Второй тестовый вектор из RFC 8032 от начала до конца. Rng здесь нет: Ed25519 выводит nonce из зерна и сообщения, так что подпись — чистая функция. Зерно — долгоживущий ключ, поэтому public_key и sign принимают его как &Secret[[u8; 32]], а тест расходует коробку через Secret::wipe. Тестовый вектор — единственное место, где константа законно становится секретом.

use std::crypto::ed25519;

test rfc8032_test2_sign_and_verify {
    let seed = Secret::wrap([
        0x4cu8, 0xcdu8, 0x08u8, 0x9bu8, 0x28u8, 0xffu8, 0x96u8, 0xdau8,
        0x9du8, 0xb6u8, 0xc3u8, 0x46u8, 0xecu8, 0x11u8, 0x4eu8, 0x0fu8,
        0x5bu8, 0x8au8, 0x31u8, 0x9fu8, 0x35u8, 0xabu8, 0xa6u8, 0x24u8,
        0xdau8, 0x8cu8, 0xf6u8, 0xedu8, 0x4fu8, 0xb8u8, 0xa6u8, 0xfbu8,
    ]);
    let public = ed25519::public_key(&seed);
    assert public == [
        0x3du8, 0x40u8, 0x17u8, 0xc3u8, 0xe8u8, 0x43u8, 0x89u8, 0x5au8,
        0x92u8, 0xb7u8, 0x0au8, 0xa7u8, 0x4du8, 0x1bu8, 0x7eu8, 0xbcu8,
        0x9cu8, 0x98u8, 0x2cu8, 0xcfu8, 0x2eu8, 0xc4u8, 0x96u8, 0x8cu8,
        0xc0u8, 0xcdu8, 0x55u8, 0xf1u8, 0x2au8, 0xf4u8, 0x66u8, 0x0cu8,
    ];

    let msg: [u8; 1] = [0x72u8];
    let sig = ed25519::sign(&seed, public, &msg);
    Secret::wipe(seed);                    // линейность: израсходован и затёрт
    assert sig == [
        0x92u8, 0xa0u8, 0x09u8, 0xa9u8, 0xf0u8, 0xd4u8, 0xcau8, 0xb8u8,
        0x72u8, 0x0eu8, 0x82u8, 0x0bu8, 0x5fu8, 0x64u8, 0x25u8, 0x40u8,
        0xa2u8, 0xb2u8, 0x7bu8, 0x54u8, 0x16u8, 0x50u8, 0x3fu8, 0x8fu8,
        0xb3u8, 0x76u8, 0x22u8, 0x23u8, 0xebu8, 0xdbu8, 0x69u8, 0xdau8,
        0x08u8, 0x5au8, 0xc1u8, 0xe4u8, 0x3eu8, 0x15u8, 0x99u8, 0x6eu8,
        0x45u8, 0x8fu8, 0x36u8, 0x13u8, 0xd0u8, 0xf1u8, 0x1du8, 0x8cu8,
        0x38u8, 0x7bu8, 0x2eu8, 0xaeu8, 0xb4u8, 0x30u8, 0x2au8, 0xeeu8,
        0xb0u8, 0x0du8, 0x29u8, 0x16u8, 0x12u8, 0xbbu8, 0x0cu8, 0x00u8,
    ];

    assert ed25519::verify(public, &msg, sig);

    let mut forged = msg;
    forged[0] = 0x73u8;
    assert !ed25519::verify(public, &forged, sig);
}

Чего здесь нет и где это искать

  • Сам TLS — это пакет, а не часть std. В std есть выработка ключей, уровень записей и кодек сообщений рукопожатия (std::encoding::tls13); конечный автомат рукопожатия, хранилище доверенных сертификатов и объект соединения живут в пакете TLS. То же относится к HPKE, разделению секрета по Шамиру, DNSSEC и протоколам консенсуса: всё, у чего есть конечный автомат протокола, оформляется пакетом.

  • DER и PEM — кодировки, а не криптография: std::encoding::der и std::encoding::pem, о них рассказывает глава Сериализация и кодировки. x509 и x509_build лежат в crypto, потому что проверяют и создают подписи.

  • std::hash не криптографический. В нём FNV-1a и CRC-32C для хеш-таблиц и контрольных сумм; чтобы что-то аутентифицировать, берите hmac.

  • Генератора случайных чисел внутри std::crypto нет. Случайность — это полномочие, поэтому она приходит как капабилити Rng, а вместе с происхождением — как Entropy. Криптографическая функция, которой нужен nonce, принимает его параметром, и всё дерево остаётся чистым и проверяемым; собственным схемам подписи из std nonce не нужен вовсе.