openwallet-foundation / sd-jwt-js

A JavaScript implementation of the Selective Disclosure JWT (SD-JWT) spec.
https://sdjwt.js.org/
Apache License 2.0
44 stars 13 forks source link

JOSE Verification Fails with ES256 Key in sd-jwt-example #250

Closed y12studio closed 1 month ago

y12studio commented 1 month ago

When attempting to verify an SD-JWT in sd-jwt-example with the jose library, a TypeError is thrown: "Invalid key for this operation, its asymmetricKeyType must be ed25519 or ed448". This occurs even though the key is generated using ES256.generateKeyPair() and the SD-JWT is successfully issued.

To Reproduce

  1. Create a Dockerfile with the following contents:
FROM node:22.9

WORKDIR /app

COPY basic.ts /app/

RUN <<EOF
npm init -y
npm i @sd-jwt/core @sd-jwt/crypto-nodejs @sd-jwt/types jose
EOF

CMD [ "node", "--experimental-strip-types","basic.ts" ]
  1. Create a basic.ts file with the following contents:
import { SDJwtInstance } from '@sd-jwt/core';
import type { DisclosureFrame } from '@sd-jwt/types';
import { ES256, digest, generateSalt } from '@sd-jwt/crypto-nodejs';
import * as jose from 'jose'

(async () => {
  const { privateKey, publicKey } = await ES256.generateKeyPair();
  const signer = await ES256.getSigner(privateKey)
  const verifier = await ES256.getVerifier(publicKey)

  const sdjwt = new SDJwtInstance({
    signer,
    verifier,
    signAlg: 'EdDSA', // Note: signAlg is EdDSA, but ES256 keys are used.
    hasher: digest,
    hashAlg: 'SHA-256',
    saltGenerator: generateSalt,
  });

  // Issuer Define the claims object with the user's information
  const claims = {
    firstname: 'John',
    lastname: 'Doe',
    ssn: '123-45-6789',
    id: '1234',
  };

  // Issuer Define the disclosure frame to specify which claims can be disclosed
  const disclosureFrame: DisclosureFrame<typeof claims> = {
    _sd: ['firstname', 'lastname', 'ssn'],
  };

  const credential = await sdjwt.issue(claims, disclosureFrame);

  console.log("Issued Credential: ",credential);
  console.log("PublicKey: ", publicKey);
  const decodeCredential = await sdjwt.decode(credential);
  console.log("Decode SD-JWT: ",decodeCredential);

  //  The serialized format for the SD-JWT is the concatenation of each
  // part delineated with a single tilde ('~') character as follows:
  // <Issuer-signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Disclosure N>~
  const issuerSignedJwt = credential.split("~")[0];

  try {
    const joseResult = await jose.jwtVerify(issuerSignedJwt, publicKey);
    console.log("JOSE RESULT: ", joseResult)
  } catch (error) {
    console.error("JOSE Verification Error:", error);
  }

})();
  1. Build and run the Docker container:
docker build -t sd-jwt-test .
docker run --rm sd-jwt-test

Expected behavior

The jose.jwtVerify function should successfully verify the SD-JWT using the provided ES256 public key.

Actual behavior

The jose.jwtVerify function throws a TypeError.

...
Issued Credential:  eyJhbGciOiJFZERTQSJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiTzVpNkpDOGx4M296em1hdUZIQ3lFaTQ1ekdkTTdjOHVXaGF3SlFZODlfTSIsIlBlcXRkSmIxbl9VVTl1MDlQVmZRak9tMndzbzBOamEyTEx2R2p5X1NLMk0iLCJfdTRDaDAtbFpQaXAyMVFkOXQyRkVNVHdsdWJMLU9hTFhZT1k1dU5BUjA0Il0sIl9zZF9hbGciOiJTSEEtMjU2In0.Eo5PR6thdfUArq9sro-086v8oFHU_0Dir0mt2fGQtEh_ppLk0cIrX4RyKybv3yh83JNMVmeNd2tIfN5VT3zuWA~WyJmZDAyNDNhODM0MzNiM2M5IiwiZmlyc3RuYW1lIiwiSm9obiJd~WyJjY2M2NTk0ODMyMGMwYmZiIiwibGFzdG5hbWUiLCJEb2UiXQ~WyJkOWY4OTU4YTIwMDg0YWY3Iiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ~
PublicKey:  {
  key_ops: [ 'verify' ],
  ext: true,
  kty: 'EC',
  x: 'n7TtUwnB_ZhxEsvQGPbupmpFWcAmSiTtax92ruTf1ZY',
  y: 'Pg4__e5MUr1na5bvAKRMspzqtQE_JLyzg1jCDAAkZjA',
  crv: 'P-256'
}
Decode SD-JWT:  _SDJwt {
  jwt: _Jwt {
    header: { alg: 'EdDSA' },
    payload: { id: '1234', _sd: [Array], _sd_alg: 'SHA-256' },
    signature: 'Eo5PR6thdfUArq9sro-086v8oFHU_0Dir0mt2fGQtEh_ppLk0cIrX4RyKybv3yh83JNMVmeNd2tIfN5VT3zuWA',
    encoded: 'eyJhbGciOiJFZERTQSJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiTzVpNkpDOGx4M296em1hdUZIQ3lFaTQ1ekdkTTdjOHVXaGF3SlFZODlfTSIsIlBlcXRkSmIxbl9VVTl1MDlQVmZRak9tMndzbzBOamEyTEx2R2p5X1NLMk0iLCJfdTRDaDAtbFpQaXAyMVFkOXQyRkVNVHdsdWJMLU9hTFhZT1k1dU5BUjA0Il0sIl9zZF9hbGciOiJTSEEtMjU2In0.Eo5PR6thdfUArq9sro-086v8oFHU_0Dir0mt2fGQtEh_ppLk0cIrX4RyKybv3yh83JNMVmeNd2tIfN5VT3zuWA'
  },
  disclosures: [
    _Disclosure {
      _digest: 'PeqtdJb1n_UU9u09PVfQjOm2wso0Nja2LLvGjy_SK2M',
      _encoded: 'WyJmZDAyNDNhODM0MzNiM2M5IiwiZmlyc3RuYW1lIiwiSm9obiJd',
      salt: 'fd0243a83433b3c9',
      key: 'firstname',
      value: 'John'
    },
    _Disclosure {
      _digest: '_u4Ch0-lZPip21Qd9t2FEMTwlubL-OaLXYOY5uNAR04',
      _encoded: 'WyJjY2M2NTk0ODMyMGMwYmZiIiwibGFzdG5hbWUiLCJEb2UiXQ',
      salt: 'ccc65948320c0bfb',
      key: 'lastname',
      value: 'Doe'
    },
    _Disclosure {
      _digest: 'O5i6JC8lx3ozzmauFHCyEi45zGdM7c8uWhawJQY89_M',
      _encoded: 'WyJkOWY4OTU4YTIwMDg0YWY3Iiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ',
      salt: 'd9f8958a20084af7',
      key: 'ssn',
      value: '123-45-6789'
    }
  ],
  kbJwt: undefined
}
JOSE Verification Error: TypeError: Invalid key for this operation, its asymmetricKeyType must be ed25519 or ed448
    at keyForCrypto (file:///app/node_modules/jose/dist/node/esm/runtime/node_key.js:47:23)
    at verify (file:///app/node_modules/jose/dist/node/esm/runtime/verify.js:21:22)
    at flattenedVerify (file:///app/node_modules/jose/dist/node/esm/jws/flattened/verify.js:92:28)
    at compactVerify (file:///app/node_modules/jose/dist/node/esm/jws/compact/verify.js:15:28)
    at Module.jwtVerify (file:///app/node_modules/jose/dist/node/esm/jwt/verify.js:5:28)
    at file:///app/basic.ts:49:35

Environment

Additional context

The issue seems to stem from a mismatch between the specified signing algorithm (signAlg: 'EdDSA') and the key type (ES256). While the SD-JWT library may be generating a valid JWT with an ES256 signature despite the signAlg setting, the jose library is expecting an EdDSA key for verification because of that signAlg value. This suggests a potential bug either in the SD-JWT library's handling of the signAlg option, or a misconfiguration in the provided example code. Correcting the signAlg to ES256 should resolve the issue.

https://github.com/openwallet-foundation-labs/sd-jwt-js/blob/96e76a9d553bff34274b5ad243d0154cd220061b/examples/sd-jwt-example/all.ts#L12

lukasjhan commented 1 month ago

Hi @y12studio Thanks for reporting.

I understood that there was a problem with the JWT signed part of SD-JWT with our crypto-nodejs not being verified by jose. Right?

It maybe a problem of our crypto-nodejs package. I'll take a look and work on the fix :)

lukasjhan commented 1 month ago

Oh I got it.

as you pointed out:

const sdjwt = new SDJwtInstance({
    signer,
    verifier,
    signAlg: 'EdDSA', // Note: signAlg is EdDSA, but ES256 keys are used.
    hasher: digest,
    hashAlg: 'SHA-256',
    saltGenerator: generateSalt,
  });

We're using EdDSA in header but signed with ES256. I'll change the example and create PR