在Javascript使用EdDSA算法签名消息获取JWT

Sign a message with EdDSA algorithm in Javascript to get JWT

我需要使用 EdDSA 算法获取 JWT 才能使用 API。我有私钥来签署消息,我可以用 PHP 和下一个库来做到这一点:https://github.com/firebase/php-jwt(你可以在 README 中看到 EdDSA 的例子)。现在我需要在 JS 中做同样的事情,但我没有找到使用给定密钥(编码的 base 64)获取 JWT 的方法(仅示例不是真正的密钥):

const secretKey = Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==

我尝试了很多库,如 jose、js-nacl、crypto、libsodium 等。我真的很接近使用 libsodium 库获得 JWT,现在附上代码:

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}
};

问题出在 sodium.crypto_sign_detached 函数中,因为它 returns 一个 Uint8Array(64) 签名,我需要这样的 JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ

如何更改 Uint8Array(64) 以获取正确格式的签名以获取 JWT?我尝试使用 base64、base64url、hex、text、ascii 等,但最终的 JWT 无效(因为签名错误)。 如果您将我的代码与我在 PHP 中提到的代码进行比较,但是 JS 库中的函数 sodium.crypto_sign_detached returns Uint8Array(64) 和 PHP 中的函数相同returns 一个字符串,我可以获得令牌。 或者也许有一种方法可以调整我给定的私钥以用于其他库(例如 crypto 或 jose,我收到了私钥格式错误) 谢谢!

在发布的 NodeJS 代码中存在以下问题:

  • crypto_sign_detached() returns 签名为 Uint8Array,可以用 Buffer.from() 导入并用 base64url() 转换为 Base64 字符串。
  • 连接 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 是确定性的,因此可以通过比较两个 JWT 来检查 NodeJS 代码:如果在上面的 NodeJS 代码中使用与 PHP 代码中相同的 header 和有效载荷,则相同的签名和因此 same JWT 是由 PHP 代码生成的,即:

eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ

这表明 NodeJS 代码有效。


请注意,可以使用 Date.now() 而不是 moment 包。这将 return 以毫秒为单位的时间,因此该值必须除以 1000,例如Math.round(Date.now()/1000),但是保存了一个依赖。