Проверка Ed25519 для iOS на облачном Mac: векторы и байты сообщения

Проверка Ed25519 для iOS на облачном Mac: векторы и байты сообщения

При передаче настроек подписи между командами из разных регионов нередко возникает ситуация: «сервер успешно подписывает данные, но проверка на iOS не проходит». Обе стороны показывают одинаковый JSON, а повторная генерация ключей ничего не меняет. Чтобы отделить ошибки вызова криптографических API от проблем декодирования при передаче и восстановления сообщения, сначала нужно создать на облачном Mac эталонную проверку, не зависящую от API приложения, а затем сверить исходные байты. Ниже используются Swift и CryptoKit; для проверки нужны только открытые тестовые данные, без рабочих закрытых ключей.

Сначала зафиксируйте алгоритм и контракт входных данных

Договоритесь об использовании обычного Ed25519: открытый ключ передаётся в необработанном виде длиной 32 байта, подпись — в необработанном виде длиной 64 байта. В CryptoKit этому соответствует Curve25519.Signing.PublicKey. Не заменяйте его типом для согласования ключей только из-за похожего названия.

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

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

Также необходимо различать обычный Ed25519 и Ed25519ph. Если сначала вычислить хеш сообщения, а затем передать его обычному API, это не будет эквивалентно использованию варианта с предварительным хешированием. Нельзя опускать эти условия в протоколе на том основании, что «это всё Ed25519».

Выполните минимальную проверку на открытом стандартном векторе

На облачном Mac с уже установленными инструментами разработки выполните xcrun swift --version, сохраните сведения о наборе инструментов и вывод sw_vers. Сохраните приведённый ниже код в verify.swift, затем запустите xcrun swift verify.swift. В коде используется тестовый вектор для пустого сообщения из RFC 8032; открытые шестнадцатеричные данные не являются рабочими ключами.

import Foundation
import CryptoKit

enum FixtureError: Error {
    case invalidHex
    case checkFailed
}

func hex(_ text: String) throws -> Data {
    let chars = Array(text.utf8)
    guard chars.count % 2 == 0 else {
        throw FixtureError.invalidHex
    }
    var bytes = [UInt8]()
    for offset in stride(from: 0, to: chars.count, by: 2) {
        let pair = String(
            decoding: chars[offset..<(offset + 2)],
            as: UTF8.self
        )
        guard let value = UInt8(pair, radix: 16) else {
            throw FixtureError.invalidHex
        }
        bytes.append(value)
    }
    return Data(bytes)
}

func require(_ value: Bool) throws {
    guard value else {
        throw FixtureError.checkFailed
    }
}

let publicBytes = try hex(
    "d75a980182b10ab7d54bfed3c964073a0" +
    "ee172f3daa62325af021a68f707511a"
)
let signature = try hex(
    "e5564300c360ac729086e2cc806e828a8" +
    "4877f1eb8e5d974d873e0652249015555" +
    "5fb8821590a33bacc61e39701cf9b46bd" +
    "25bf5f0595bbe24655141438e7a100b"
)
try require(publicBytes.count == 32)
try require(signature.count == 64)

let key = try Curve25519.Signing.PublicKey(
    rawRepresentation: publicBytes
)
try require(key.isValidSignature(signature, for: Data()))
try require(!key.isValidSignature(
    signature, for: Data([0x00])
))

var altered = signature
altered[altered.startIndex] ^= 1
try require(!key.isValidSignature(altered, for: Data()))
try require(!key.isValidSignature(
    Data(signature.dropLast()), for: Data()
))

var rejectedKey = false
do {
    _ = try Curve25519.Signing.PublicKey(
        rawRepresentation: Data(publicBytes.dropLast())
    )
} catch {
    rejectedKey = true
}
try require(rejectedKey)

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

Сведите различия в тексте к различиям в байтах

Сохраните один файл с сообщением до подписания и другой — с данными, фактически полученными клиентом. Открытые тестовые файлы можно сравнить с помощью cmp, а затем найти первое расхождение через xxd -g 1. Рабочее тело ответа может содержать конфиденциальные сведения, поэтому его не следует напрямую записывать в общие журналы.

Начните с того, что легко меняется в редакторе: нет ли лишнего перевода строки в конце, используется ли LF или CRLF, присутствует ли в начале файла UTF-8 BOM, применялась ли к символам нормализация Unicode. Даже визуально одинаковые символы с диакритическими знаками могут иметь разные последовательности байтов.

Не создавайте файлы с помощью echo без проверки: эта команда может добавить перевод строки. Читайте сообщение как исходные данные файла, а не преобразуйте его сначала в строку, а затем обратно в данные. Если протокол действительно предусматривает подписание строк, явно задайте UTF-8 и подготовьте отдельные тестовые примеры для завершающего перевода строки, пустого сообщения и комбинируемых символов.

Отдельно проверьте транспортный формат

Если API передаёт данные в Base64, сначала декодируйте их, затем проверьте длину. 128 шестнадцатеричных символов — это не подпись длиной 128 байт. Необработанный открытый ключ также не равнозначен файлу ключа с заголовками: нельзя передавать все данные вместе с обрамлением непосредственно в rawRepresentation.

Не допускайте подстановки пустых данных при ошибке декодирования с последующим продолжением проверки. Разделяйте внутренние типы ошибок на «некорректный формат обрамления», «неверная длина» и «подпись не совпадает», но не раскрывайте наружу лишние подробности разбора.

Подготовьте негативные тесты, выявляющие ошибки интеграции

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

На уровне приложения добавьте пример, в котором тело сообщения верно, но подпись относится к предыдущему сообщению. Это позволит проверить, не использует ли код ошибочно закешированный результат. Если подписываемое содержимое включает версию, срок действия или идентификатор назначения, убедитесь, что эти поля действительно входят в подписываемые байты, а не просто находятся во внешнем неподписанном объекте.

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

Завершите передачу между регионами комплектом проверочных данных

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

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

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

Часто задаваемые вопросы

Нужен ли закрытый ключ для проверки на клиенте?

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

Почему одинаковые по смыслу JSON не проходят проверку?

Ed25519 проверяет байты, а не смысл JSON. Порядок ключей, пробелы и представление Unicode меняют сообщение. Проверяйте исходные байты либо согласуйте единый формат канонизации.

Выберите облачный Mac для следующей задачи

Выберите сервер в регионе, где находится основная часть команды, и получите в полное распоряжение физический Mac mini. Для удалённого рабочего стола в браузере нужны современный браузер и стабильное подключение к интернету. Перед заказом проверьте конфигурацию, период оплаты и фактическую доступность.

Подключайтесь ближе к команде

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