Ed25519 относится к классу эллиптических цифровых подписей с высокой скоростью вычисления и устойчивостью к типичным ошибкам реализации. В контексте JavaScript экосистемы он часто используется через реализации NaCl, такие как TweetNaCl.js, где доступен набор криптографических примитивов, достаточных для построения подписей и проверки целостности данных без привлечения внешних криптографических сервисов.
JWT-подобные токены обычно состоят из трёх частей: заголовка, полезной нагрузки и подписи. Даже если формат не является строго RFC 7519-совместимым, структура сохраняется:
Ключевая особенность при использовании Ed25519 заключается в том, что
подпись формируется над байтовым представлением строки
base64url(header) + "." + base64url(payload), а не над JSON
напрямую.
JWT-подобные структуры не используют стандартный base64, так как он
содержит символы +, / и padding
=. Вместо этого применяется base64url:
function base64urlEncode(buffer) {
return Buffer.from(buffer)
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
function base64urlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
const pad = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4));
return Buffer.from(str + pad, 'base64');
}
В браузерной среде вместо Buffer используется
Uint8Array и atob/btoa, но логика
трансформации сохраняется.
JWT-подобный токен в упрощённой форме:
base64url(header).base64url(payload).base64url(signature)
Заголовок фиксирует алгоритм:
const header = {
alg: "Ed25519",
typ: "JWT"
};
Полезная нагрузка содержит произвольные claims:
const payload = {
sub: "user-123",
iat: Math.floor(Date.now() / 1000),
role: "admin"
};
Сериализация:
const encodedHeader = base64urlEncode(JSON.stringify(header));
const encodedPayload = base64urlEncode(JSON.stringify(payload));
const signingInput = `${encodedHeader}.${encodedPayload}`;
TweetNaCl.js предоставляет реализацию Ed25519 через функции
nacl.sign и nacl.sign.detached.
Для подписания JWT-подобного токена используется именно detached-режим, поскольку требуется отдельно хранить подпись:
import nacl from "tweetnacl";
nacl.util = require("tweetnacl-util");
function signToken(payload, secretKey) {
const header = {
alg: "Ed25519",
typ: "JWT"
};
const encodedHeader = base64urlEncode(JSON.stringify(header));
const encodedPayload = base64urlEncode(JSON.stringify(payload));
const data = `${encodedHeader}.${encodedPayload}`;
const messageBytes = nacl.util.decodeUTF8(data);
const signature = nacl.sign.detached(messageBytes, secretKey);
const encodedSignature = base64urlEncode(signature);
return `${data}.${encodedSignature}`;
}
Здесь secretKey — это 64-байтовый приватный ключ
Ed25519, включающий seed и публичную часть, как это принято в
NaCl-формате.
Верификация строится на том же принципе: восстановление исходной строки и проверка подписи через публичный ключ.
function verifyToken(token, publicKey) {
const parts = token.split(".");
if (parts.length !== 3) return false;
const [encodedHeader, encodedPayload, encodedSignature] = parts;
const data = `${encodedHeader}.${encodedPayload}`;
const messageBytes = nacl.util.decodeUTF8(data);
const signatureBytes = base64urlDecode(encodedSignature);
return nacl.sign.detached.verify(
messageBytes,
signatureBytes,
publicKey
);
}
Проверка не восстанавливает приватный ключ и не расшифровывает данные, а только подтверждает, что подпись соответствует сообщению и публичному ключу.
Ed25519 отличается от RSA и ECDSA рядом свойств, которые напрямую влияют на архитектуру токенов:
Из-за этого отпадает необходимость в сложных параметрах, характерных для ECDSA, где ошибки генерации nonce приводят к компрометации ключей.
TweetNaCl.js использует бинарные ключи фиксированного размера:
Генерация:
const keyPair = nacl.sign.keyPair();
const publicKey = keyPair.publicKey;
const secretKey = keyPair.secretKey;
Хранение ключей требует строгого разделения: приватный ключ не должен попадать в клиентский код при моделях с серверной подписью токенов.
При использовании Ed25519 в формате JWT-подобных структур возникают архитектурные особенности:
Последний пункт критичен: различие в порядке ключей JSON или пробелах меняет байтовую последовательность, что приводит к невалидной подписи. Поэтому сериализация должна быть строго детерминированной.
Для стабильной подписи применяется сортировка ключей:
function stableStringify(obj) {
if (obj === null || typeof obj !== "object") {
return JSON.stringify(obj);
}
if (Array.isArray(obj)) {
return `[${obj.map(stableStringify).join(",")}]`;
}
const keys = Object.keys(obj).sort();
return `{${keys.map(k => `"${k}":${stableStringify(obj[k])}`).join(",")}}`;
}
Использование:
const encodedPayload = base64urlEncode(stableStringify(payload));
Хотя структура напоминает JWT, различия принципиальны:
Это приводит к тому, что такие токены чаще рассматриваются как “JWT-like”, а не стандартные JWT.
Процесс формирования и проверки обычно разделяется на две стороны:
Генерация токена:
Проверка:
TweetNaCl.js компактен и не требует WebCrypto API, что делает его применимым в средах с ограниченным доступом к криптографическим функциям платформы.
При этом все операции происходят синхронно и детерминированно, что упрощает интеграцию в event-driven архитектуры фронтенда.
Некорректные реализации чаще всего связаны не с самим Ed25519, а с обвязкой:
Каждая из этих ошибок приводит к либо невалидным токенам, либо к полной компрометации модели доверия.
Архитектурно подпись JWT-подобных токенов на Ed25519 обычно размещается в отдельном сервисе, который выполняет только одну задачу — криптографическое подписание и верификацию. Бизнес-логика при этом не должна участвовать в формировании ключей или сериализации, кроме формирования исходного payload.