Мультиподпись: схемы с несколькими подписантами

В TweetNaCl.js используется реализация Ed25519 — схемы цифровой подписи на основе эллиптических кривых (EdDSA). Базовый набор операций включает:

  • генерацию ключевой пары (секретный и публичный ключ)
  • подпись сообщения
  • проверку подписи

Подпись в классической модели представляет собой результат функции:

  • signature = Sign(privateKey, message)
  • Verify(publicKey, message, signature) → true/false

Эта модель строго одно-подписантная и не предусматривает нативной поддержки мультиподписи. Любые мультиподписные схемы в экосистеме NaCl/TweetNaCl.js строятся поверх базовых примитивов.


Проблема мультиподписи в Ed25519

Классическая схема цифровой подписи предполагает наличие одного владельца секретного ключа. В мультиподписных сценариях требуется выполнение условия:

  • несколько участников совместно подписывают одно сообщение
  • результатом является единая подпись, которую можно проверить одним публичным ключом

Ключевые требования:

  • невозможность подмены участника
  • защита от подделки частичных подписей
  • детерминированная агрегация ключей или подписей
  • отсутствие необходимости доверять централизованному координатору

В NaCl/TweetNaCl.js отсутствует встроенный механизм мультиподписи, поэтому применяются схемы композиции поверх Ed25519.


Наивная схема: конкатенация подписей

Самый простой подход — каждый участник подписывает сообщение отдельно.

Механизм

Для участников A, B, C:

  1. каждый получает одно и то же сообщение m

  2. каждый вычисляет подпись:

    • sA = Sign(a_private, m)
    • sB = Sign(b_private, m)
    • sC = Sign(c_private, m)
  3. итоговая структура:

    • S = {sA, sB, sC}

Проверка

Проверяются все подписи независимо:

  • Verify(A_pub, m, sA)
  • Verify(B_pub, m, sB)
  • Verify(C_pub, m, sC)

Недостатки

  • линейный рост размера подписи
  • отсутствие единого идентификатора подписи
  • невозможность использования как единичной криптографической сущности
  • высокая стоимость хранения и передачи

Мультиподпись через агрегированный публичный ключ

Следующий шаг — создание общего публичного ключа группы.

Идея

Публичные ключи участников комбинируются в один:

  • P = P1 + P2 + ... + Pn

В контексте Ed25519 прямое сложение не всегда безопасно без дополнительных протоколов, но для учебных моделей используется упрощённая схема.

Подпись

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

  • каждый создаёт частичную подпись
  • координатор агрегирует результат

Проблема

Ed25519 не является схемой, где безопасна простая линейная агрегация ключей без протоколов защиты от rogue-key атак.


Безопасная модель: протокол с фиксацией публичных ключей

Чтобы избежать атак подмены ключей, вводится обязательная стадия фиксации списка участников.

Фаза инициализации

Все участники согласуют:

  • список публичных ключей P1...Pn
  • порядок участников (канонический порядок сортировки)
  • доменное сообщение (domain separation tag)

Пример:

group = hash(P1 || P2 || P3)

Частичные подписи и комбинирование

Каждый участник создаёт частичную подпись:

  • Si = Sign(private_i, message || group_id)

Далее выполняется агрегация:

  • S = combine(S1, S2, ..., Sn)

В NaCl/TweetNaCl.js это реализуется вручную как структура данных:

const multisig = {
  message,
  groupId,
  signatures: [sigA, sigB, sigC],
  publicKeys: [pkA, pkB, pkC]
}

Проверка:

  • каждая подпись валидируется отдельно
  • проверяется соответствие groupId
  • проверяется полнота набора подписей

Threshold-схемы (t-of-n)

Более сложный класс — пороговые схемы, где требуется не вся группа, а только часть.

Определение

  • n участников
  • порог t
  • подпись действительна, если собрано ≥ t подписей

Пример 2-of-3

Любые два участника могут подписать сообщение.


Реализация threshold поверх TweetNaCl.js

NaCl не поддерживает threshold natively, поэтому используется конструкция:

1. Распределение ключей

Каждый участник имеет:

  • private key share xi

2. Частичные подписи

Каждый участник создаёт:

  • si = Sign(xi, message)

3. Комбинация

Сборщик формирует:

  • список подписей
  • bitmap участников
const thresholdSig = {
  msg,
  sigs: [
    { id: 1, sig: sig1 },
    { id: 3, sig: sig3 }
  ]
}

4. Проверка

Проверка проходит по правилам:

  • минимум t валидных подписей
  • каждый sig проверяется отдельно

MuSig-подобные схемы (адаптация идеи)

MuSig — современная схема мультиподписи для Schnorr-подписей. Ed25519 не является Schnorr напрямую, но можно имитировать поведение.

Основная идея

  • агрегированный публичный ключ
  • агрегированный nonce
  • единая подпись

Проблема в TweetNaCl.js

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


Практическая реализация в JavaScript поверх TweetNaCl.js

Установка

import nacl from 'tweetnacl'
import util from 'tweetnacl-util'

Генерация ключей

const keyA = nacl.sign.keyPair()
const keyB = nacl.sign.keyPair()
const keyC = nacl.sign.keyPair()

Подпись участниками

const msg = util.decodeUTF8("important message")

const sigA = nacl.sign.detached(msg, keyA.secretKey)
const sigB = nacl.sign.detached(msg, keyB.secretKey)
const sigC = nacl.sign.detached(msg, keyC.secretKey)

Структура мультиподписи

const multisignature = {
  message: msg,
  publicKeys: [
    keyA.publicKey,
    keyB.publicKey,
    keyC.publicKey
  ],
  signatures: [sigA, sigB, sigC]
}

Проверка

function verifyMulti(msig) {
  return msig.signatures.every((sig, i) => {
    return nacl.sign.detached.verify(
      msig.message,
      sig,
      msig.publicKeys[i]
    )
  })
}

Атаки и криптографические риски

Rogue-key атака

Если не фиксировать набор публичных ключей, участник может:

  • подменить свой ключ
  • подобрать ключ, компенсирующий чужой

Решение:

  • обязательное хеширование списка ключей

Replay атакa

Подпись может быть повторно использована.

Решение:

  • включение nonce / timestamp в сообщение
message = msg || timestamp || sessionId

Отсутствие агрегированной подписи

NaCl не даёт компактного представления мультиподписи, что приводит к:

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

Оптимизированная схема хранения

Для практических систем используется структура:

{
  version: 1,
  algorithm: "ed25519-multisig",
  groupHash: "...",
  message: "...",
  sigs: [...],
  bitmap: "101"
}

bitmap позволяет:

  • быстро проверять участие
  • поддерживать частичное подписание

Протокол координации подписантов

Этапы

  1. инициализация группы
  2. обмен публичными ключами
  3. фиксация groupHash
  4. получение сообщения
  5. параллельное подписание
  6. сбор подписей
  7. проверка агрегата

Масштабирование схем

При увеличении числа участников возникают проблемы:

  • рост O(n) по размеру подписи
  • рост O(n²) по коммуникации (в координированных схемах)

Типичные оптимизации:

  • батч-проверка подписей
  • параллельная верификация
  • кэширование publicKey hash

Применение мультиподписей на TweetNaCl.js

Сценарии:

  • корпоративное одобрение транзакций
  • распределённые администраторские права
  • DAO-голосование
  • контроль доступа к криптографическим ключам
  • подпись конфигураций в распределённых системах

Ограничения NaCl-экосистемы

  • отсутствие нативного threshold cryptography
  • отсутствие MuSig/aggregate signatures
  • отсутствие стандартизированных схем multisig
  • необходимость ручной сборки протоколов

Это приводит к тому, что мультиподпись в TweetNaCl.js всегда является протокольной надстройкой, а не встроенной функцией криптографической библиотеки.