From c44107f580744292a987ba2cda5795e443aaa9df Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Thu, 21 Dec 2023 19:09:50 +0100 Subject: [PATCH] feat: EBSI compatibility --- .../client/lib/__tests__/EBSIE2E.spec.test.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 packages/client/lib/__tests__/EBSIE2E.spec.test.ts diff --git a/packages/client/lib/__tests__/EBSIE2E.spec.test.ts b/packages/client/lib/__tests__/EBSIE2E.spec.test.ts new file mode 100644 index 00000000..5af36248 --- /dev/null +++ b/packages/client/lib/__tests__/EBSIE2E.spec.test.ts @@ -0,0 +1,132 @@ +import { Alg, CodeChallengeMethod, Jwt } from '@sphereon/oid4vci-common' +import { toJwk } from '@sphereon/ssi-sdk-ext.key-utils' +import { CredentialMapper } from '@sphereon/ssi-types' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +import { from } from '@trust/keyto' +import { fetch } from 'cross-fetch' +import debug from 'debug' +import { base64url, importJWK, JWK, SignJWT } from 'jose' +import * as u8a from 'uint8arrays' + +import { OpenID4VCIClient } from '..' + +export const UNIT_TEST_TIMEOUT = 30000; + +const ISSUER_URL = 'https://conformance-test.ebsi.eu/conformance/v3/issuer-mock'; +const AUTH_URL = 'https://conformance-test.ebsi.eu/conformance/v3/auth-mock'; + +const jwk: JWK = { + alg: 'ES256', + use: 'sig', + kty: 'EC', + crv: 'P-256', + x: 'hUWYK06qFvdudydiqnEhVJhZ-73jcLtuzH8kIyNOSHE', + y: 'UZf7oUkJdo65SQekMD5ssiRclEimG2SmlsjXf3QwQJo', + d: 'zDeeo3K0Pk8dofeKcajvJYxMZ1vijx_cVDJQl1IpbAM', +}; + +console.log(`JWK (private/orig): ${JSON.stringify(jwk, null, 2)}`); + +const privateKey = from(jwk, 'jwk').toString('blk', 'private'); +const publicKey = from(jwk, 'jwk').toString('blk', 'public'); +console.log(`Private key: ${privateKey}`); +console.log(`Public key: ${publicKey}`); +console.log(`Private key (b64): ${base64url.encode(u8a.fromString(privateKey, 'base16'))}`); +console.log(`JWK (private 2) ${JSON.stringify(toJwk(privateKey, 'Secp256r1', { isPrivateKey: true }))}`); +console.log(`JWK (public 2) ${JSON.stringify(toJwk(publicKey, 'Secp256r1', { isPrivateKey: false }))}`); + +// const DID_METHOD = 'did:key' +const DID = + 'did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrm54tL4pRrDDhR1QJ5RHPMXUq5MzYpZL2k35vya5eMiNxschNy9AJ74CC3MmcYiZJGZfyhWQ6qDgTVcDSHdquwPYvLDut383JbrgYdZYYSC2merTMgmQtUi3huYhaky1qE'; +const DID_URL_ENCODED = + 'did%3Akey%3Az2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrm54tL4pRrDDhR1QJ5RHPMXUq5MzYpZL2k35vya5eMiNxschNy9AJ74CC3MmcYiZJGZfyhWQ6qDgTVcDSHdquwPYvLDut383JbrgYdZYYSC2merTMgmQtUi3huYhaky1qE'; +// const PRIVATE_KEY_HEX = '7dd923e40f4615ac496119f7e793cc2899e99b64b88ca8603db986700089532b' + +// const PUBLIC_KEY_HEX = +// '04a23cb4c83901acc2eb0f852599610de0caeac260bf8ed05e7f902eaac0f9c8d74dd4841b94d13424d32af8ec0e9976db9abfa7e3a59e10d565c5d4d901b4be63' + +// pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9 +// priv hex: 913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5 +// const did = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`; +const kid = `${DID}#z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrm54tL4pRrDDhR1QJ5RHPMXUq5MzYpZL2k35vya5eMiNxschNy9AJ74CC3MmcYiZJGZfyhWQ6qDgTVcDSHdquwPYvLDut383JbrgYdZYYSC2merTMgmQtUi3huYhaky1qE`; + +// const jw = jose.importKey() +describe('OID4VCI-Client using Sphereon issuer should', () => { + async function test(credentialType: 'CTWalletCrossPreAuthorised' | 'CTWalletCrossInTime') { + debug.enable('*'); + const offer = await getCredentialOffer(credentialType); + const client = await OpenID4VCIClient.fromURI({ + uri: offer, + kid, + alg: Alg.ES256, + clientId: DID_URL_ENCODED + }); + expect(client.credentialOffer).toBeDefined(); + expect(client.endpointMetadata).toBeDefined(); + expect(client.getCredentialEndpoint()).toEqual(`${ISSUER_URL}/credential`); + expect(client.getAccessTokenEndpoint()).toEqual(`${AUTH_URL}/token`); + + if (credentialType !== 'CTWalletCrossPreAuthorised') { + const url = client.createAuthorizationRequestUrl({redirectUri: 'openid4vc%3A', codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', codeChallengeMethod: CodeChallengeMethod.SHA256}) + const result = await fetch(url) + console.log(result.text()) + } + + const accessToken = await client.acquireAccessToken({ pin: '0891' }); + // console.log(accessToken); + expect(accessToken).toMatchObject({ + expires_in: 86400, + // scope: 'GuestCredential', + token_type: 'Bearer', + }); + + const format = 'jwt_vc'; + const credentialResponse = await client.acquireCredentials({ + credentialTypes: client.getCredentialOfferTypes()[0], + format, + proofCallbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + kid, + }); + console.log(JSON.stringify(credentialResponse, null, 2)) + expect(credentialResponse.credential).toBeDefined(); + const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credentialResponse.credential!); + expect(format.startsWith(wrappedVC.format)).toEqual(true); + } + + it( + 'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json', + async () => { + await test('CTWalletCrossPreAuthorised') + // await test('CTWalletCrossInTime'); + }, + UNIT_TEST_TIMEOUT, + ); +}); + +async function getCredentialOffer(credentialType: 'CTWalletCrossPreAuthorised' | 'CTWalletCrossInTime'): Promise { + const credentialOffer = await fetch( + `https://conformance-test.ebsi.eu/conformance/v3/issuer-mock/initiate-credential-offer?credential_type=${credentialType}&client_id=${DID_URL_ENCODED}&credential_offer_endpoint=openid-credential-offer%3A%2F%2F`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + ); + + return await credentialOffer.text(); +} + +async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise { + const importedJwk = await importJWK(jwk); + return await new SignJWT({ ...args.payload }) + .setProtectedHeader({ ...args.header, kid: kid! }) + .setIssuer(DID) + .setIssuedAt() + .setExpirationTime('2m') + .sign(importedJwk); +}