Подпишите сообщение с помощью алгоритма EdDSA в Javascript, чтобы получить JWT
Мне нужно получить JWT с алгоритмом EdDSA, чтобы иметь возможность использовать API. У меня есть закрытый ключ для подписи сообщения, и я могу сделать это с помощью PHP со следующей библиотекой: https://github.com/firebase/php-jwt (вы можете увидеть пример с EdDSA в README). Теперь мне нужно сделать то же самое в JS, но я не нашел способа получить JWT с заданным секретным ключом (закодированным base 64) вот так (только пример не настоящий secretKey):
const secretKey = Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==
Я пробовал много библиотек, таких как jose, js-nacl, crypto, libsodium и т. д. И я действительно близок к тому, чтобы получить JWT с библиотекой libsodium, теперь я прикрепляю код:
const base64url = require("base64url");
const _sodium = require("libsodium-wrappers");
const moment = require("moment");
const getJWT = async () => {
await _sodium.ready;
const sodium = _sodium;
const privateKey =
"Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==";
const payload = {
iss: "test",
aud: "test.com",
iat: 1650101178,
exp: 1650101278,
sub: "12345678-1234-1234-1234-123456789123"
};
const { msg, keyAscii} = encode(payload, privateKey, "EdDSA");
const signature = sodium.crypto_sign_detached(msg, keyDecoded); //returns Uint8Array(64)
//Here is the problem.
};
const encode = (payload, key, alg) => {
const header = {
typ: "JWT",
alg //'EdDSA'
};
const headerBase64URL = base64url(JSON.stringify(header));
const payloadBase64URL = base64url(JSON.stringify(payload));
const headerAndPayloadBase64URL = `${headerBase64URL}.${payloadBase64URL}`;
const keyAscii= Buffer.from(key, "base64").toString("ascii");
return {headerAndPayloadBase64URL , keyAscii}
};
Проблема заключается в функции натрия.crypto_sign_detached, потому что она возвращает подпись Uint8Array(64), и мне нужен такой JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
Как я могу изменить Uint8Array(64), чтобы получить подпись в правильном формате для получения JWT? Я пробовал с base64, base64url, hex, text, ascii и т. д., и окончательный JWT недействителен (потому что подпись неверна). Если вы сравните мой код с кодом, который я упомянул, с PHP он очень похож, но функция натрия.crypto_sign_detached возвращает Uint8Array(64) в библиотеке JS, и та же функция в PHP возвращает строку, и я могу получить токен. Или, может быть, есть способ адаптировать мой закрытый ключ для использования в другой библиотеке (например, crypto или jose, где я получил сообщение об ошибке для формата закрытого ключа). Спасибо!
1 ответ
В опубликованном коде NodeJS есть следующие проблемы:
- возвращает подпись как
Uint8Array
, который можно импортировать с помощьюBuffer.from()
и преобразован в строку Base64 с помощьюbase64url()
. - Объединение
headerAndPayloadBase64URL
и подпись в кодировке Base64url с.
поскольку разделитель дает JWT, который вы ищете. - Необработанный закрытый ключ не должен быть декодирован с помощью
'ascii'
, так как это обычно искажает данные. Вместо этого его следует просто обрабатывать как буфер. Примечание. Если по какой-либо причине требуется преобразование в строку, используйте'binary'
как кодировку, которая создает строку байтов (однако это не вариант сcrypto_sign_detached()
так как эта функция ожидает буфер).
С этими изменениями получается следующий код NodeJS:
const _sodium = require('libsodium-wrappers');
const base64url = require("base64url");
const getJWT = async () => {
await _sodium.ready;
const sodium = _sodium;
const privateKey = "Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==";
const payload = {
iss: "test",
aud: "test.com",
iat: 1650101178,
exp: 1650101278,
sub: "12345678-1234-1234-1234-123456789123"
};
const {headerAndPayloadBase64URL, keyBuf} = encode(payload, privateKey, "EdDSA");
const signature = sodium.crypto_sign_detached(headerAndPayloadBase64URL, keyBuf);
const signatureBase64url = base64url(Buffer.from(signature));
console.log(`${headerAndPayloadBase64URL}.${signatureBase64url}`) // eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
};
const encode = (payload, key, alg) => {
const header = {
typ: "JWT",
alg //'EdDSA'
};
const headerBase64URL = base64url(JSON.stringify(header));
const payloadBase64URL = base64url(JSON.stringify(payload));
const headerAndPayloadBase64URL = `${headerBase64URL}.${payloadBase64URL}`;
const keyBuf = Buffer.from(key, "base64");
return {headerAndPayloadBase64URL, keyBuf};
};
getJWT();
Тест:
поскольку Ed25519 является детерминированным, код NodeJS можно проверить, сравнив оба JWT: если, как в приведенном выше коде NodeJS, используются тот же заголовок и полезная нагрузка, что и в коде PHP, создается та же подпись и, следовательно, тот же JWT. как по коду PHP, а именно:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
который показывает, что код NodeJS работает.
Обратите внимание, что вместо
moment
упаковка,
Date.now()
может быть использован. Это вернет время в миллисекундах, поэтому значение должно быть разделено на 1000, например
Math.round(Date.now()/1000)
, но сохраняет зависимость.