GOOGLE ADS

sábado, 30 de abril de 2022

Firme un mensaje con el algoritmo EdDSA en Javascript para obtener JWT

Necesito obtener JWT con el algoritmo EdDSA para poder usar una API. Tengo la clave privada para firmar el mensaje y podría hacerlo con PHP con la siguiente biblioteca: https://github.com/firebase/php-jwt (puede ver el ejemplo con EdDSA en README). Ahora necesito hacer lo mismo en JS pero no encontré la manera de obtener JWT con una clave secreta dada (base codificada 64) así (solo un ejemplo no es la clave secreta real):

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

Probé muchas bibliotecas como jose, js-nacl, crypto, libsodium, etc. Y estoy muy cerca de obtener el JWT con la biblioteca libsodium, ahora adjunto el código:

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

El problema está en la función de sodio.crypto_sign_tached porque devuelve una firma Uint8Array(64) y necesito el JWT así:

eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ

¿Cómo puedo cambiar Uint8Array (64) para obtener la firma en un formato correcto para obtener el JWT? Probé con base64, base64url, hex, text, ascii, etc. y el JWT final no es válido (porque la firma es incorrecta). Si compara mi código con el código que mencioné con PHP es muy similar, pero la función de sodio.crypto_sign_tached devuelve Uint8Array (64) en la biblioteca JS y la misma función en PHP devuelve una cadena y puedo obtener el token. O tal vez haya una manera de adaptar mi clave privada dada para usarla en otra biblioteca (como crypto o jose donde recibí un error para el formato de clave privada) ¡Gracias!


Solución del problema

En el código NodeJS publicado hay los siguientes problemas:


  • crypto_sign_detached()devuelve la firma como un Uint8Array, que se puede importar Buffer.from()y convertir a una cadena Base64 con base64url().

  • La concatenación headerAndPayloadBase64URLy la firma codificada en Base64url con un .separador as proporciona el JWT que está buscando.

  • La clave privada sin procesar no debe decodificarse en ASCII, ya que esto generalmente la corrompe. En su lugar, simplemente debe manejarse como un búfer. Nota: Si es absolutamente necesaria una conversión a una cadena, utilícela 'binary'como codificación, lo que crea una cadena de bytes. Sin embargo, esto no es aplicable aquí, ya crypto_sign_detached()que no maneja cadenas de bytes.


Con estos cambios, el siguiente código NodeJS proporciona el mismo JWT que el código PHP:

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();

Dado que Ed25519 es determinista, el código se puede verificar comparando ambos JWT. Para esto, por supuesto, se debe usar ese payload que también se usó al crear el JWT con el código PHP. Esto se ha tenido en cuenta en el código anterior.

Tenga en cuenta que en lugar del momentpaquete, Date.now()podría usarse. Esto devolverá el tiempo en milisegundos, por lo que el valor debe dividirse por 1000, por ejemplo Math.round(Date.now()/1000), pero guarda una dependencia.

JWT/EdDSA no suele ser compatible con las herramientas en línea. Una posibilidad de verificación es, por ejemplo, con Python y PyJWT (ver esta publicación ):

import jwt
# X.509 counterpart to the base64 encoded raw key: wEzrdKRsPdIxvAUBwhNxDo4gpBUdv4GluiVovt52afA=
publicKey = """-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAwEzrdKRsPdIxvAUBwhNxDo4gpBUdv4GluiVovt52afA=
-----END PUBLIC KEY-----"""
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ'
decoded = jwt.decode(token, publicKey, audience="test.com", algorithms='EdDSA', verify=True, options={"verify_exp": False})
print(decoded) # {'iss': 'test', 'aud': 'test.com', 'iat': 1650101178, 'exp': 1650101278, 'sub': '12345678-1234-1234-1234-123456789123'}

Sin embargo, tenga en cuenta que el token publicado expiró el sábado 16 de abril de 2022 a las 09:27:58 GMT+0000 ( exp: 1650101278), por lo que la verificación de este JWT en particular solo es posible si la verificación de la fecha de vencimiento está deshabilitada con options={"verify_exp": False}.

No hay comentarios:

Publicar un comentario

Regla de Firestore para acceder a la generación de subcolección Permisos faltantes o insuficientes

Tengo problemas con las reglas de Firestore para permitir el acceso a algunos recursos en una subcolección. Tengo algunos requests document...