From 5d8cd35554f748a4f2798da265cc6713f1121a24 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Thu, 8 Aug 2024 03:59:39 +0100 Subject: [PATCH] Fix JWT verification, add did:jwk resolver (often used in SIOP JWT-VCs) --- package.json | 3 +- src/resolver/did/did-jwk-resolver.js | 20 ++++++ src/resolver/did/index.js | 1 + src/utils/vc/contexts.js | 6 ++ .../contexts/sphereon-wallet-identity-v1.json | 17 +++++ src/utils/vc/credentials.js | 38 ++++++----- tests/unit/jwt-vc.test.js | 45 +++++++++++++ yarn.lock | 66 ++++++++++++++++++- 8 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 src/resolver/did/did-jwk-resolver.js create mode 100644 src/utils/vc/contexts/sphereon-wallet-identity-v1.json create mode 100644 tests/unit/jwt-vc.test.js diff --git a/package.json b/package.json index 15b37029a..1bba63aa7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/sdk", - "version": "9.1.0", + "version": "9.2.0", "main": "index.js", "license": "MIT", "repository": { @@ -108,6 +108,7 @@ "@docknetwork/node-types": "^0.17.0", "@juanelas/base64": "^1.0.5", "@polkadot/api": "10.12.4", + "@sphereon/ssi-sdk-ext.did-resolver-jwk": "^0.24.0", "@transmute/json-web-signature": "^0.7.0-unstable.80", "base64url": "3.0.1", "blake2b": "2.1.4", diff --git a/src/resolver/did/did-jwk-resolver.js b/src/resolver/did/did-jwk-resolver.js new file mode 100644 index 000000000..82800441c --- /dev/null +++ b/src/resolver/did/did-jwk-resolver.js @@ -0,0 +1,20 @@ +import { getDidJwkResolver } from '@sphereon/ssi-sdk-ext.did-resolver-jwk'; +import DIDResolver from './did-resolver'; + +const jwkResolver = getDidJwkResolver(); + +export default class DIDJWKResolver extends DIDResolver { + static METHOD = 'jwk'; + + constructor() { + super(undefined); + } + + async resolve(did) { + const { didDocument } = await jwkResolver.jwk(did); + return { + '@context': 'https://www.w3.org/ns/did/v1', + ...didDocument, + }; + } +} diff --git a/src/resolver/did/index.js b/src/resolver/did/index.js index d384719a2..2efab98f3 100644 --- a/src/resolver/did/index.js +++ b/src/resolver/did/index.js @@ -1,4 +1,5 @@ export { default as DIDResolver } from './did-resolver'; export { default as DIDKeyResolver } from './did-key-resolver'; +export { default as DIDJWKResolver } from './did-jwk-resolver'; export { default as DockDIDResolver } from './dock-did-resolver'; export { default as UniversalResolver } from './universal-resolver'; diff --git a/src/utils/vc/contexts.js b/src/utils/vc/contexts.js index bb7468a14..d90e9e52b 100644 --- a/src/utils/vc/contexts.js +++ b/src/utils/vc/contexts.js @@ -16,6 +16,7 @@ import dockPrettyVCContext from './contexts/prettyvc.json'; import jws2020V1Context from './contexts/jws-2020-v1.json'; import statusList21Context from './contexts/status-list-21'; import privateStatusList21Context from './contexts/private-status-list-21'; +import sphereonId from './contexts/sphereon-wallet-identity-v1.json'; // Lookup of following URLs will lead to loading data from the context directory, this is done as the Sr25519 keys are not // supported in any W3C standard and vc-js has them stored locally. This is a temporary solution. @@ -105,4 +106,9 @@ export default new Map([ 'https://ld.dock.io/private-status-list-21', privateStatusList21Context, ], + // Overriden due to 404ing + [ + 'https://sphereon-opensource.github.io/ssi-mobile-wallet/context/sphereon-wallet-identity-v1.jsonld', + sphereonId, + ], ]); diff --git a/src/utils/vc/contexts/sphereon-wallet-identity-v1.json b/src/utils/vc/contexts/sphereon-wallet-identity-v1.json new file mode 100644 index 000000000..65ac305b9 --- /dev/null +++ b/src/utils/vc/contexts/sphereon-wallet-identity-v1.json @@ -0,0 +1,17 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "swi": "https://sphereon-opensource.github.io/ssi-mobile-wallet/context/sphereon-wallet-identity-v1#", + "schema": "https://schema.org/", + "SphereonWalletIdentityCredential": "swi:SphereonWalletIdentityCredential", + "id": "@id", + "type": "@type", + "firstName": "swi:firstName", + "lastName": "swi:lastName", + "emailAddress": { + "@id": "swi:emailAddress", + "@type": "schema:email" + } + } +} diff --git a/src/utils/vc/credentials.js b/src/utils/vc/credentials.js index 509302ce8..5ded91e25 100644 --- a/src/utils/vc/credentials.js +++ b/src/utils/vc/credentials.js @@ -158,7 +158,7 @@ export function checkCredentialJSONLD(credential) { * @param {object} credential - An object that could be a VerifiableCredential. * @throws {Error} */ -export function checkCredentialRequired(credential) { +export function checkCredentialRequired(credential, isJWT) { // Ensure first context is DEFAULT_CONTEXT_V1_URL if (credential['@context'][0] !== DEFAULT_CONTEXT_V1_URL) { throw new Error( @@ -176,21 +176,23 @@ export function checkCredentialRequired(credential) { throw new Error('"credentialSubject" property is required.'); } - // Ensure issuer is valid - const issuer = getId(credential.issuer); - if (!issuer) { - throw new Error( - `"issuer" must be an object with ID property or a string. Got: ${credential.issuer}`, - ); - } else if (!issuer.includes(':')) { - throw new Error('"issuer" id must be in URL format.'); - } + // Ensure issuer and issue date is valid, only for non-jwt as that is defined in the header + if (!isJWT) { + const issuer = getId(credential.issuer); + if (!issuer) { + throw new Error( + `"issuer" must be an object with ID property or a string. Got: ${credential.issuer}`, + ); + } else if (!issuer.includes(':')) { + throw new Error('"issuer" id must be in URL format.'); + } - // Ensure there is an issuance date, if exists - if (!credential.issuanceDate) { - throw new Error('"issuanceDate" property is required.'); - } else { - ensureValidDatetime(credential.issuanceDate); + // Ensure there is an issuance date, if exists + if (!credential.issuanceDate) { + throw new Error('"issuanceDate" property is required.'); + } else { + ensureValidDatetime(credential.issuanceDate); + } } } @@ -219,8 +221,8 @@ export function checkCredentialOptional(credential) { * @param {object} credential - An object that could be a VerifiableCredential. * @throws {Error} */ -export function checkCredential(credential) { - checkCredentialRequired(credential); +export function checkCredential(credential, isJWT) { + checkCredentialRequired(credential, isJWT); checkCredentialOptional(credential); checkCredentialJSONLD(credential); } @@ -285,7 +287,7 @@ export async function verifyCredential( } // Check credential is valid - checkCredential(credential); + checkCredential(credential, isJWT); // Check expiration date if (verifyDates && 'expirationDate' in credential) { diff --git a/tests/unit/jwt-vc.test.js b/tests/unit/jwt-vc.test.js new file mode 100644 index 000000000..336139dfe --- /dev/null +++ b/tests/unit/jwt-vc.test.js @@ -0,0 +1,45 @@ +// Mock fetch +import mockFetch from '../mocks/fetch'; + +import { + verifyCredential, + verifyPresentation, +} from '../../src/utils/vc/index'; +import DIDJWKResolver from '../../src/resolver/did/did-jwk-resolver'; + +mockFetch(); + +const SPHEREON_ID_JWT_CREDENTIAL = 'eyJraWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lSRlZqTUVwMVNuRjFNbFV5U1dGNVN6TXlOMFJzVjE5b05VcHJPRzlqUmxSbVVsQktRVGxNTUVwQlVTSXNJbmtpT2lJd01qSlBWMk5IYmtvNFJFUmZkbmhGTFY5UldUSmhURUZQZUZSdVlUVjFabmRpWWpkMVNFRnhSM0YzSW4wIzAiLCJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc3BoZXJlb24tb3BlbnNvdXJjZS5naXRodWIuaW8vc3NpLW1vYmlsZS13YWxsZXQvY29udGV4dC9zcGhlcmVvbi13YWxsZXQtaWRlbnRpdHktdjEuanNvbmxkIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTcGhlcmVvbldhbGxldElkZW50aXR5Q3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJUZXN0IiwibGFzdE5hbWUiOiJUZXN0IiwiZW1haWxBZGRyZXNzIjoidGVzdEB0ZXN0LmNvbSJ9fSwic3ViIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0luVnpaU0k2SW5OcFp5SXNJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pUkZWak1FcDFTbkYxTWxVeVNXRjVTek15TjBSc1YxOW9OVXByT0c5alJsUm1VbEJLUVRsTU1FcEJVU0lzSW5raU9pSXdNakpQVjJOSGJrbzRSRVJmZG5oRkxWOVJXVEpoVEVGUGVGUnVZVFYxWm5kaVlqZDFTRUZ4UjNGM0luMCIsImp0aSI6InVybjp1dWlkOmNiOWUzYzZhLTZjOTYtNGFiYS1iNWY0LWFiM2RmMDM4Y2MyMiIsIm5iZiI6MTcyMjk3NDM5OSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0luVnpaU0k2SW5OcFp5SXNJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pUkZWak1FcDFTbkYxTWxVeVNXRjVTek15TjBSc1YxOW9OVXByT0c5alJsUm1VbEJLUVRsTU1FcEJVU0lzSW5raU9pSXdNakpQVjJOSGJrbzRSRVJmZG5oRkxWOVJXVEpoVEVGUGVGUnVZVFYxWm5kaVlqZDFTRUZ4UjNGM0luMCJ9.Y9CBYHA_sgfA_V40i69SYrqsAK1OZ6rUW8NlrZwavbPxcVS_LX3tFvRRU0jkslUbuf7rColxf2f8zo-YMan-_w'; + +// Test constants +const vpId = 'https://example.com/credentials/12345'; +const vpHolder = 'https://example.com/credentials/1234567890'; + +function getSamplePres(presentationCredentials) { + return { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: presentationCredentials, + id: vpId, + holder: vpHolder, + }; +} + +describe('Static JWT-VC verification', () => { + const resolver = new DIDJWKResolver(); + + test('Sphereon ID credential', async () => { + const result = await verifyCredential(SPHEREON_ID_JWT_CREDENTIAL, { + resolver, + }); + expect(result.verified).toBe(true); + }); + + test('Sphereon ID credential in presentation', async () => { + const result = await verifyPresentation(getSamplePres([SPHEREON_ID_JWT_CREDENTIAL]), { + resolver, + unsignedPresentation: true, + }); + expect(result.verified).toBe(true); + }); +}); diff --git a/yarn.lock b/yarn.lock index a5f6464c2..f2fbb5f12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4714,6 +4714,27 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== +"@sd-jwt/decode@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.6.1.tgz#141f7782df53bab7159a75d91ed4711e1c14a7ea" + integrity sha512-QgTIoYd5zyKKLgXB4xEYJTrvumVwtsj5Dog0v0L9UH9ZvHekDaeexS247X7A4iSdzTvmZzUpGskgABOa4D8NmQ== + dependencies: + "@sd-jwt/types" "0.6.1" + "@sd-jwt/utils" "0.6.1" + +"@sd-jwt/types@0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.6.1.tgz#fc4235e00cf40d35a21d6bc02e44e12d7162aa9b" + integrity sha512-LKpABZJGT77jNhOLvAHIkNNmGqXzyfwBT+6r+DN9zNzMx1CzuNR0qXk1GMUbast9iCfPkGbnEpUv/jHTBvlIvg== + +"@sd-jwt/utils@0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.6.1.tgz#33273b20c9eb1954e4eab34118158b646b574ff9" + integrity sha512-1NHZ//+GecGQJb+gSdDicnrHG0DvACUk9jTnXA5yLZhlRjgkjyfJLNsCZesYeCyVp/SiyvIC9B+JwoY4kI0TwQ== + dependencies: + "@sd-jwt/types" "0.6.1" + js-base64 "^3.7.6" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -4757,6 +4778,27 @@ str2buf "^1.3.0" webcrypto-shim "^0.1.7" +"@sphereon/ssi-sdk-ext.did-resolver-jwk@^0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-sdk-ext.did-resolver-jwk/-/ssi-sdk-ext.did-resolver-jwk-0.24.0.tgz#7603028542d1e1f13d13975a436150b0d0e6de43" + integrity sha512-74to/yE8q3A7O935AlvHWhvBItHuC0HCiKq2UiYa5Adw3NVU9mElXUOik7IRJrhuD6wJNaAxDmQYAFIWRdIG1g== + dependencies: + "@sphereon/ssi-types" "0.28.0" + base64url "^3.0.1" + debug "^4.3.4" + did-resolver "^4.1.0" + uint8arrays "^3.1.1" + +"@sphereon/ssi-types@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.28.0.tgz#3eccb6dd7be19af26c5820885f35f3232d038a2d" + integrity sha512-NkTkrsBoQUZzJutlk5XD3snBxL9kfsxKdQvBbGUEaUDOiW8siTNUoJuQFeA+bI0eJY99up95bmMKdJeDc1VDfg== + dependencies: + "@sd-jwt/decode" "^0.6.1" + debug "^4.3.5" + events "^3.3.0" + jwt-decode "^3.1.2" + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -6776,6 +6818,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.5: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + decimal.js@^10.2.0: version "10.4.2" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.2.tgz#0341651d1d997d86065a2ce3a441fbd0d8e8b98e" @@ -6909,6 +6958,11 @@ did-resolver@2.1.1: resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-2.1.1.tgz#43796f8a3e921644e5fb15a8147684ca87019cfd" integrity sha512-FYLTkNWofjYNDGV1HTQlyVu1OqZiFxR4I8KM+oxGVOkbXva15NfWzbzciqdXUDqOhe6so5aroAdrVip6gSAYSA== +did-resolver@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-4.1.0.tgz#740852083c4fd5bf9729d528eca5d105aff45eb6" + integrity sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA== + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -9689,6 +9743,11 @@ jose@^4.3.8: resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.5.tgz#6475d0f467ecd3c630a1b5dadd2735a7288df706" integrity sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg== +js-base64@^3.7.6: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + js-sha256@0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" @@ -9922,6 +9981,11 @@ just-debounce-it@^3.0.1: resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-3.2.0.tgz#4352265f4af44188624ce9fdbc6bff4d49c63a80" integrity sha512-WXzwLL0745uNuedrCsCs3rpmfD6DBaf7uuVwaq98/8dafURfgQaBsSpjiPp5+CW6Vjltwy9cOGI6qE71b3T8iQ== +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + keccak@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" @@ -12734,7 +12798,7 @@ uint8arrays@^2.0.5: dependencies: multiformats "^9.4.2" -uint8arrays@^3.0.0: +uint8arrays@^3.0.0, uint8arrays@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==