Подпишите сообщение с помощью алгоритма 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), но сохраняет зависимость.

Другие вопросы по тегам