В криптографических протоколах композиция операций подписи и
шифрования определяет не только свойства безопасности, но и границы
того, что именно защищается: содержимое сообщения, его происхождение,
либо оба свойства одновременно. В контексте TweetNaCl.js / nacl.js эти
схемы реализуются поверх примитивов подписи (nacl.sign) и
симметричного или асимметричного шифрования
(nacl.secretbox, nacl.box), которые сами по
себе не задают строгого протокольного порядка применения.
Схема encrypt-then-sign предполагает, что сначала сообщение шифруется, а затем полученный шифротекст подписывается.
Формально:
C = Enc(K_enc, M)
S = Sign(K_sign, C)
Передача:
(C, S)
На стороне получателя:
S над CCПодпись накладывается на уже зашифрованные данные. Это означает, что:
Типичный подход с nacl.sign.detached и
nacl.box:
const nonce = nacl.randomBytes(nacl.box.nonceLength);
const ciphertext = nacl.box(
message,
nonce,
recipientPublicKey,
senderSecretKey
);
const signature = nacl.sign.detached(
ciphertext,
senderSigningSecretKey
);
const packet = {
nonce,
ciphertext,
signature
};
Проверка:
const valid = nacl.sign.detached.verify(
ciphertext,
signature,
senderSigningPublicKey
);
if (!valid) {
throw new Error("Invalid signature");
}
const message = nacl.box.open(
ciphertext,
nonce,
senderPublicKey,
recipientSecretKey
);
Схема sign-then-encrypt предполагает сначала создание подписи над открытым текстом, после чего сообщение вместе с подписью шифруется.
Формально:
S = Sign(K_sign, M)
P = (M, S)
C = Enc(K_enc, P)
Передача:
C
На стороне получателя:
C → (M, S)S над MПодпись скрыта внутри шифротекста и становится доступной только после расшифрования.
const signature = nacl.sign.detached(
message,
senderSigningSecretKey
);
const payload = new Uint8Array(message.length + signature.length);
payload.set(message);
payload.set(signature, message.length);
const nonce = nacl.randomBytes(nacl.box.nonceLength);
const ciphertext = nacl.box(
payload,
nonce,
recipientPublicKey,
senderSecretKey
);
Расшифрование:
const payload = nacl.box.open(
ciphertext,
nonce,
senderPublicKey,
recipientSecretKey
);
const message = payload.slice(0, payload.length - nacl.sign.signatureLength);
const signature = payload.slice(payload.length - nacl.sign.signatureLength);
const valid = nacl.sign.detached.verify(
message,
signature,
senderSigningPublicKey
);
if (!valid) {
throw new Error("Invalid signature");
}
encrypt-then-sign:
sign-then-encrypt:
В NaCl-экосистеме (и в TweetNaCl.js как минималистичной реализации) важно учитывать, что базовые примитивы разделены:
nacl.box обеспечивает только шифрование и
аутентификацию между двумя сторонамиnacl.sign обеспечивает цифровую подпись без
шифрованияКомбинация этих примитивов не навязывает порядок, но выбор схемы влияет на:
encrypt-then-sign чаще используется как более строгая схема композиции, поскольку позволяет выстроить фильтрацию неподлинных сообщений до выполнения криптографически дорогих операций расшифрования.
Различие схем связано не только с порядком операций, но и с тем, где именно фиксируется целостность данных:
Эти уровни приводят к разному поведению при повторном использовании ciphertext, модификациях и попытках анализа трафика.
В системах, построенных на TweetNaCl.js, encrypt-then-sign часто используется в сценариях, где:
Sign-then-encrypt встречается в архитектурах, где:
При неправильной реализации обе схемы могут деградировать до небезопасных конструкций:
nacl.box полностью
разрушает конфиденциальностьВ NaCl-подобных библиотеках ключевое значение имеет не только выбор схемы, но и строгая дисциплина обработки: разделение ключей, неизменяемость nonce и детерминированный порядок проверок.