web-token / jwt-framework

JWT Framework
MIT License
886 stars 105 forks source link

JWT signed with EdDSA fails to be validated with nodejs/jose #492

Closed ericchaves closed 7 months ago

ericchaves commented 9 months ago

Version(s) affected

3.2.8

Description

Hi folks, I'm issuing a JWT signed with EdDSA (ed25519) using web-token\jwt-framework and trying to validate it using nodej's jose but the validation fails complaining that signature is invalid.

With jwt-framework I can verify a token issued and signed by nodej's jose using the same EdDSA key and I can also verify with jwt-framework a JWT signed by jwt-framework in jwt-framework with this EdDSA key.

Other EC algorithms like ES256 also works perfectly between the two libs. I can also validate other tokens issued in other languages using EdDSA in nodejs. So far only EdDSA tokens signed in PHP are failing.

Can someone help me figure out if I'm doing something wrong or if there is some interoperability issue between those two implementations?

Thanks in advance for any help and congrats for the great work done so far!

How to reproduce

Forgive me if you find any typos. Had to copy and paste partial lines of code.

create an ed25519 key pair using openssl:

openssl genpkey -algorithm Ed25519 -out ed25519_private_key.pem
openssl pkcs8 -topk8 -nocrypt -in ed25519_private_key.pem -out ed25519_private_key_pkcs8.pem
openssl pkey -in ed25519_private_key.pem -pubout -out ed25519_public_key.pem

Issue a signed JWS with web-token/jwt-framework and write it to file.

use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Core\JWK;
use Jose\Component\Signature\Algorithm\EdDSA;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer as JWSCompactSerializer;
use Jose\Component\Signature\Serializer\JWSSerializer;

//CONST ALG = 'ES256';
//const CHAVE_PRIVADA_PKCS8  = './keys/ES256/ecdsa-p256_private_key_pkcs8.pem';
//const CHAVE_PUBLICA        = './keys/ES256/ecdsa-p256_public_key.pem';

CONST ALG = 'EdDSA';
const CHAVE_PRIVADA_PKCS8 = './keys/EdDSA/ed25519_private_key_pkcs8.pem';
const CHAVE_PUBLICA = './keys/EdDSA/ed25519_public_key.pem';

$chavePrivada = JWKFactory::createFromKeyFile(CHAVE_PRIVADA_PKCS8);
$chavePublica = JWKFactory::createFromKeyFile(CHAVE_PUBLICA);

$payload = [
    'exp' => time() + 60 * 50,
    'iss' => 'rebels',
    'sub' => '78286616731',
    'aud' => 'ghost',
    'iat' => time(),
    'name' => 'Ezra Bridger',
    'deviceid' => '3c512309-57ca-47bf-8543-c2f1cec1189a',
];

$algorithmManager = new AlgorithmManager([
    new ES256(),
    new EdDSA(),
]);
$jwsBuilder = new JWSBuilder($algorithmManager);
$jws = $jwsBuilder
    ->create()
    ->withPayload(json_encode($payload))
    ->addSignature($chavePrivada, ['alg' => ALG])
    ->build();

$serializer = new JWSCompactSerializer();
$token = $serializer->serialize($jws, 0);
file_put_contents('./output/php-jws.'. ALG . '.txt', $token);

// $token = file_get_contents('./output/node-jws.txt');
$jwsVerifier = new JWSVerifier($algorithmManager);
$jwsVerifier->verifyWithKey($jws, $chavePrivada, 0);

read it on nodejs and validate the JWT

import { jwtVerify,  importSPKI, decodeProtectedHeader } from 'jose';
import { readFileSync } from 'fs';

async function validateJWT(filename, publicKeyFile) {
  const jwt = readFileSync(filename).toString();
  const protectedHeader = decodeProtectedHeader(jwt);
  console.log(protectedHeader);
  const publicKeyPartner = await importSPKI(readFileSync(publicKeyFile).toString());
  const verifiedJWT = await jwtVerify(jwt, publicKeyPartner);
  return verifiedJWT;
}
validateJWT('./output/php-jws.ES256.txt', './keys/ES256/ecdsa-p256_public_key.pem');
validateJWT('./output/php-jws.EdDSA.txt', './keys/EdDSA/ed25519_public_key.pem');

To generate ES256 keys with openssl:

openssl ecparam -genkey -name prime256v1 -noout -out ecdsa-p256-private.pem
openssl pkcs8 -topk8 -nocrypt -in ecdsa-p256-private.pem -out ecdsa-p256-private.p8
openssl ec -in ecdsa-p256-private.pem -pubout -out ecdsa-p256-public.pem

Possible Solution

No response

Additional Context

Node version: v20.5.0 node jose version: 4.13.1 Error message from nodes

file:///home/user/jwt/node_modules/jose/dist/node/esm/jws/flattened/verify.js:87
        throw new JWSSignatureVerificationFailed();
              ^

JWSSignatureVerificationFailed: signature verification failed
    at flattenedVerify (file:///home/user/jwt/node_modules/jose/dist/node/esm/jws/flattened/verify.js:87:15)
    at async compactVerify (file:///home/user/jwt/node_modules/jose/dist/node/esm/jws/compact/verify.js:15:22)
    at async jwtVerify (file:///home/user/jwt/node_modules/jose/dist/node/esm/jwt/verify.js:6:22)
    at async validateJWT (file:///home/user/jwt/node-read.mjs:9:23) {
  code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'
}
ericchaves commented 9 months ago

After doing a little more digging, it seems that something weird is going on with the JWK class when extracting the public key correctly.

The conversion of the private PKC#8 key to JWT using node's crypto module outputs to:

{"kty":"OKP",
 "crv":"Ed25519",
 "d":"9eM9ymRfGIF7wXU0alGBLQNp664KK0o4SVtDLmsMo5M",
 "x":"O2_3C89eS6Yx0U6Ak4avXY2FuBLGql6y7wXirtzsYxo",
}

the same JWK created using JWKFactory::createFromKeyFile outputs to:

[values:Jose\Component\Core\JWK:private] => Array
        (
            [kty] => OKP
            [crv] => Ed25519
            [d] => 9eM9ymRfGIF7wXU0alGBLQNp664KK0o4SVtDLmsMo5M
            [x] => Xx1RjKLDu76eIMjwIP_0oWy6axLLjL6-CUWxRRi-8xk
            [use] => sig
        )

)

the public keys does not seem to match. What is curious is that if use JWKFactory::createFromKeyFile to export the public SPKI file exported by openssl, the public key is ok.

{"kty":"OKP"
  "crv":"Ed25519",
  "x":"O2_3C89eS6Yx0U6Ak4avXY2FuBLGql6y7wXirtzsYxo"
}
Jose\Component\Core\JWK Object
(
    [values:Jose\Component\Core\JWK:private] => Array
        (
            [kty] => OKP
            [crv] => Ed25519
            [x] => O2_3C89eS6Yx0U6Ak4avXY2FuBLGql6y7wXirtzsYxo
            [use] => sig
        )
)
Spomky commented 9 months ago

Hi,

Many thanks for the detail. I will investigate. I do not see any reason for the public key to change.

Spomky commented 7 months ago

Hi,

I investigated and indeed there is something wrong with the JWK generated from the private key: the parameter x is incorrect. This is the reason why the nodejs/jose rejects the tokens signed by the library.

I spotted the code section causing this issue, but have not found any fix for now. What I suggest at the moment is to alter the x parameter from the private JWK with the good one public key.

Wrong private key:

{"kty":"OKP"
  "crv":"Ed25519",
  "d":"9eM9ymRfGIF7wXU0alGBLQNp664KK0o4SVtDLmsMo5M",
  "x":"Xx1RjKLDu76eIMjwIP_0oWy6axLLjL6-CUWxRRi-8xk"
}

Correct public key:

{"kty":"OKP"
  "crv":"Ed25519",
  "x":"O2_3C89eS6Yx0U6Ak4avXY2FuBLGql6y7wXirtzsYxo"
}

Correct private key:

{"kty":"OKP"
  "crv":"Ed25519",
  "d":"9eM9ymRfGIF7wXU0alGBLQNp664KK0o4SVtDLmsMo5M",
  "x":"O2_3C89eS6Yx0U6Ak4avXY2FuBLGql6y7wXirtzsYxo"
}
Spomky commented 7 months ago

Hi,

This should be fixed with the last release 3.2.10. Let me know if there is another issue.

Regards

github-actions[bot] commented 6 months ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.