From 2d13142f0cdb0e6c9d074b23b58e2628f41fa541 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Thu, 5 Dec 2024 19:14:20 +0100 Subject: [PATCH] chore: cleanup and small bugfix Signed-off-by: Tom Lanser --- .../src/__tests__/Oid4vciClient.test.ts | 84 +++++----- .../src/__tests__/__fixtures__/paradym.ts | 150 ++++++++++++++++++ .../proof-type/jwt/v-jwt-proof-type.ts | 5 +- .../credential-issuer-metadata.ts | 4 +- 4 files changed, 199 insertions(+), 44 deletions(-) diff --git a/packages/oid4vci/src/__tests__/Oid4vciClient.test.ts b/packages/oid4vci/src/__tests__/Oid4vciClient.test.ts index 4306b81..cf7910d 100644 --- a/packages/oid4vci/src/__tests__/Oid4vciClient.test.ts +++ b/packages/oid4vci/src/__tests__/Oid4vciClient.test.ts @@ -15,7 +15,7 @@ import { callbacks, getSignJwtCallback, parseXwwwFormUrlEncoded } from '../../.. import { AuthorizationFlow, Oid4vciClient } from '../Oid4vciClient' import { extractScopesForCredentialConfigurationIds } from '../metadata/credential-issuer/credential-configurations' import { bdrDraft13 } from './__fixtures__/bdr' -import { paradymDraft11, paradymDraft13 } from './__fixtures__/paradym' +import { paradymDraft11, paradymDraft13, paradymDraft13Federation } from './__fixtures__/paradym' import { presentationDuringIssuance } from './__fixtures__/presentationDuringIssuance' const server = setupServer() @@ -685,8 +685,13 @@ describe('Oid4vciClient', () => { }) test('receive a credential from Paradym using federation', async () => { - const { d, ...publicKeyJwk } = paradymDraft13.holderPrivateKeyJwk - const jwtSigner = getSignJwtCallback([paradymDraft13.holderPrivateKeyJwk]) + const { d, ...publicKeyJwk } = paradymDraft13Federation.holderPrivateKeyJwk + const jwtSigner = getSignJwtCallback([paradymDraft13Federation.holderPrivateKeyJwk]) + const jwkKid = await calculateJwkThumbprint({ + hashAlgorithm: HashAlgorithm.Sha256, + hashCallback: callbacks.hash, + jwk: paradymDraft13Federation.holderPrivateKeyJwk, + }) const entityConfigurationJwt = await createEntityConfiguration({ signJwtCallback: async ({ jwk, toBeSigned }) => { @@ -709,35 +714,28 @@ describe('Oid4vciClient', () => { return decodeBase64(signature) }, header: { - kid: await calculateJwkThumbprint({ - hashAlgorithm: HashAlgorithm.Sha256, - hashCallback: callbacks.hash, - jwk: paradymDraft13.holderPrivateKeyJwk, - }), + kid: jwkKid, alg: 'ES256', typ: 'entity-statement+jwt', }, claims: { exp: new Date(new Date().getTime() + 1000 * 60 * 60 * 24).getTime(), iat: new Date().getTime(), - sub: paradymDraft13.credentialIssuerMetadata.credential_issuer, - iss: paradymDraft13.credentialIssuerMetadata.credential_issuer, + sub: paradymDraft13Federation.credentialIssuerMetadata.credential_issuer, + iss: paradymDraft13Federation.credentialIssuerMetadata.credential_issuer, jwks: { keys: [ { ...publicKeyJwk, alg: 'ES256', - kid: await calculateJwkThumbprint({ - hashAlgorithm: HashAlgorithm.Sha256, - hashCallback: callbacks.hash, - jwk: paradymDraft13.holderPrivateKeyJwk, - }), + kid: jwkKid, }, ], }, metadata: { + federation_entity: paradymDraft13Federation.federationEntity, openid_provider: { - ...paradymDraft13.credentialIssuerMetadata, + ...paradymDraft13Federation.credentialIssuerMetadata, client_registration_types_supported: ['automatic'], }, }, @@ -745,35 +743,40 @@ describe('Oid4vciClient', () => { }) server.resetHandlers( - http.get(paradymDraft13.credentialOfferUri.replace('?raw=true', ''), () => - HttpResponse.json(paradymDraft13.credentialOfferObject) + http.get(paradymDraft13Federation.credentialOfferUri.replace('?raw=true', ''), () => + HttpResponse.json(paradymDraft13Federation.credentialOfferObject) ), - http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-federation`, () => - HttpResponse.text(entityConfigurationJwt, { - headers: { - 'Content-Type': 'application/entity-statement+jwt', - }, - }) + http.get( + `${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/openid-federation`, + () => + HttpResponse.text(entityConfigurationJwt, { + headers: { + 'Content-Type': 'application/entity-statement+jwt', + }, + }) ), - http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-credential-issuer`, () => - HttpResponse.json(paradymDraft13.credentialIssuerMetadata) + http.get( + `${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/openid-credential-issuer`, + () => HttpResponse.json(paradymDraft13Federation.credentialIssuerMetadata) ), - http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/openid-configuration`, () => - HttpResponse.text(undefined, { status: 404 }) + http.get( + `${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/openid-configuration`, + () => HttpResponse.text(undefined, { status: 404 }) ), - http.get(`${paradymDraft13.credentialOfferObject.credential_issuer}/.well-known/oauth-authorization-server`, () => - HttpResponse.text(undefined, { status: 404 }) + http.get( + `${paradymDraft13Federation.credentialOfferObject.credential_issuer}/.well-known/oauth-authorization-server`, + () => HttpResponse.text(undefined, { status: 404 }) ), - http.post(paradymDraft13.credentialIssuerMetadata.token_endpoint, async ({ request }) => { + http.post(paradymDraft13Federation.credentialIssuerMetadata.token_endpoint, async ({ request }) => { expect(parseXwwwFormUrlEncoded(await request.text())).toEqual({ 'pre-authorized_code': '1130293840889780123292078', grant_type: preAuthorizedCodeGrantIdentifier, resource: credentialOffer.credential_issuer, }) - return HttpResponse.json(paradymDraft13.accessTokenResponse) + return HttpResponse.json(paradymDraft13Federation.accessTokenResponse) }), - http.post(paradymDraft13.credentialIssuerMetadata.credential_endpoint, async ({ request }) => { + http.post(paradymDraft13Federation.credentialIssuerMetadata.credential_endpoint, async ({ request }) => { expect(await request.json()).toEqual({ format: 'vc+sd-jwt', vct: 'https://metadata.paradym.id/types/6fTEgFULv2-EmployeeBadge', @@ -782,7 +785,7 @@ describe('Oid4vciClient', () => { jwt: expect.any(String), }, }) - return HttpResponse.json(paradymDraft13.credentialResponse) + return HttpResponse.json(paradymDraft13Federation.credentialResponse) }) ) @@ -794,12 +797,12 @@ describe('Oid4vciClient', () => { }, }) - const credentialOffer = await client.resolveCredentialOffer(paradymDraft13.credentialOffer) - expect(credentialOffer).toStrictEqual(paradymDraft13.credentialOfferObject) + const credentialOffer = await client.resolveCredentialOffer(paradymDraft13Federation.credentialOffer) + expect(credentialOffer).toStrictEqual(paradymDraft13Federation.credentialOfferObject) const issuerMetadata = await client.resolveIssuerMetadata(credentialOffer.credential_issuer) expect(issuerMetadata.credentialIssuer).toStrictEqual({ - ...paradymDraft13.credentialIssuerMetadata, + ...paradymDraft13Federation.credentialIssuerMetadata, client_registration_types_supported: ['automatic'], }) @@ -807,12 +810,13 @@ describe('Oid4vciClient', () => { credentialOffer, issuerMetadata, }) - expect(accessTokenResponse).toStrictEqual(paradymDraft13.accessTokenResponse) - expect(authorizationServer).toStrictEqual(paradymDraft13.credentialIssuerMetadata.credential_issuer) + expect(accessTokenResponse).toStrictEqual(paradymDraft13Federation.accessTokenResponse) + expect(authorizationServer).toStrictEqual(paradymDraft13Federation.credentialIssuerMetadata.credential_issuer) const encodedJwk = Buffer.from(JSON.stringify(publicKeyJwk)).toString('base64url') const didUrl = `did:jwk:${encodedJwk}#0` + // This is the holder side that will create a proof of possession of the private key const { jwt: proofJwt } = await client.createCredentialRequestJwtProof({ issuerMetadata, signer: { @@ -850,6 +854,6 @@ describe('Oid4vciClient', () => { jwt: proofJwt, }, }) - expect(credentialResponse.credentialResponse).toStrictEqual(paradymDraft13.credentialResponse) + expect(credentialResponse.credentialResponse).toStrictEqual(paradymDraft13Federation.credentialResponse) }) }) diff --git a/packages/oid4vci/src/__tests__/__fixtures__/paradym.ts b/packages/oid4vci/src/__tests__/__fixtures__/paradym.ts index 30a5934..187f8bf 100644 --- a/packages/oid4vci/src/__tests__/__fixtures__/paradym.ts +++ b/packages/oid4vci/src/__tests__/__fixtures__/paradym.ts @@ -142,6 +142,156 @@ export const paradymDraft13 = { }, } +export const paradymDraft13Federation = { + credentialOffer: + 'https://paradym.id/invitation?credential_offer_uri=https%3A%2F%2Fparadym.id%2Finvitation%2Fdraft-13-issuer%2Foffers%2Fb99db8f1-4fa2-4b27-8dc7-ecf81478eb9b%3Fraw%3Dtrue', + credentialOfferUri: + 'https://paradym.id/invitation/draft-13-issuer/offers/b99db8f1-4fa2-4b27-8dc7-ecf81478eb9b?raw=true', + credentialOfferObject: { + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': '1130293840889780123292078', + user_pin_required: false, + }, + }, + credential_configuration_ids: ['clvi9a5od00127pap4obzoeuf'], + credential_issuer: 'https://agent.paradym.id/oid4vci/draft-13-issuer', + }, + authorizationServerMetadata: null, + credentialIssuerMetadata: { + credential_issuer: 'https://agent.paradym.id/oid4vci/draft-13-issuer', + token_endpoint: 'https://agent.paradym.id/oid4vci/draft-13-issuer/token', + credential_endpoint: 'https://agent.paradym.id/oid4vci/draft-13-issuer/credential', + credentials_supported: [ + { + format: 'vc+sd-jwt', + vct: 'https://metadata.paradym.id/types/iuoQGyxlww-ParadymContributor', + id: 'clv2gbawu000tfkrk5l067h1h', + cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'], + cryptographic_suites_supported: ['EdDSA', 'ES256'], + display: [ + { + name: 'Paradym Contributor', + description: 'Contributed to the Paradym Release', + background_color: '#5535ed', + background_image: {}, + text_color: '#ffffff', + }, + ], + }, + { + format: 'vc+sd-jwt', + vct: 'https://metadata.paradym.id/types/6fTEgFULv2-EmployeeBadge', + id: 'clvi9a5od00127pap4obzoeuf', + cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'], + cryptographic_suites_supported: ['EdDSA', 'ES256'], + display: [ + { + name: 'Employee Badge', + description: 'Credential for employee badge', + background_color: '#000000', + background_image: { url: 'https://github.com/animo.png' }, + text_color: '#ffffff', + }, + ], + }, + { + format: 'vc+sd-jwt', + vct: 'https://metadata.paradym.id/types/ULaVABcapZ-Heyo', + id: 'clx4z0auo00a6f0sibkutdqor', + cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'], + cryptographic_suites_supported: ['EdDSA', 'ES256'], + display: [ + { + name: 'Direct issuance revocation', + background_color: '#000000', + background_image: {}, + text_color: '#ffffff', + }, + ], + }, + ], + credential_configurations_supported: { + clv2gbawu000tfkrk5l067h1h: { + cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'], + credential_signing_alg_values_supported: ['EdDSA', 'ES256'], + proof_types_supported: { jwt: { proof_signing_alg_values_supported: ['EdDSA', 'ES256'] } }, + display: [ + { + name: 'Paradym Contributor', + description: 'Contributed to the Paradym Release', + background_color: '#5535ed', + background_image: {}, + text_color: '#ffffff', + }, + ], + format: 'vc+sd-jwt', + vct: 'https://metadata.paradym.id/types/iuoQGyxlww-ParadymContributor', + }, + clvi9a5od00127pap4obzoeuf: { + cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'], + credential_signing_alg_values_supported: ['EdDSA', 'ES256'], + proof_types_supported: { jwt: { proof_signing_alg_values_supported: ['EdDSA', 'ES256'] } }, + display: [ + { + name: 'Employee Badge', + description: 'Credential for employee badge', + background_color: '#000000', + background_image: { url: 'https://github.com/animo.png' }, + text_color: '#ffffff', + }, + ], + format: 'vc+sd-jwt', + vct: 'https://metadata.paradym.id/types/6fTEgFULv2-EmployeeBadge', + }, + clx4z0auo00a6f0sibkutdqor: { + cryptographic_binding_methods_supported: ['did:key', 'did:jwk', 'did:web'], + credential_signing_alg_values_supported: ['EdDSA', 'ES256'], + proof_types_supported: { jwt: { proof_signing_alg_values_supported: ['EdDSA', 'ES256'] } }, + display: [ + { + name: 'Direct issuance revocation', + background_color: '#000000', + background_image: {}, + text_color: '#ffffff', + }, + ], + format: 'vc+sd-jwt', + vct: 'https://metadata.paradym.id/types/ULaVABcapZ-Heyo', + }, + }, + display: [{ name: 'Animo', logo: { url: 'https://github.com/animo.png', alt_text: 'Logo of Animo Solutions' } }], + }, + federationEntity: { + organization_name: 'Animo', + logo_uri: 'https://github.com/animo.png', + homepage_uri: 'https://animo.id', + contacts: ['support@animo.id'], + }, + accessTokenResponse: { + access_token: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjhXWGU4UFBEVTRrTnpIbjJuZ0FTUmFsUmxiZHF6NEJjYkU3X0RTUTN0aUkifX0.eyJwcmVBdXRob3JpemVkQ29kZSI6IjExMzAyOTM4NDA4ODk3ODAxMjMyOTIwNzgiLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5wYXJhZHltLmlkL29pZDR2Y2kvOWI2ZGY1YmMtNTk2NS00YWVjLWEzOWEtMDNjYjNiMjc4NmI1IiwiZXhwIjoxNzI5NTY3ODk2LCJpYXQiOjE3Mjk1Njc3MTZ9.iBeLzXv7Z6kwGpgrT-5XoCyWOkl4FMDixVMqPbdSkLYq8eqU-iJWSWPsoqGnNhZ8B2H6zaEYKpxbZhdvSauSBg', + token_type: 'bearer', + expires_in: 180, + c_nonce: '463253917094869172078310', + c_nonce_expires_in: 300, + authorization_pending: false, + }, + credentialResponse: { + credential: + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdWFiek4xdzRxcnlINENMY1pRSmllaEpRd3VpM3V3RVloSjEzd01qWG5RTnQifQ.eyJkZXBhcnRtZW50IjoiU3RyaW5nIHZhbHVlIiwidmN0IjoiaHR0cHM6Ly9tZXRhZGF0YS5wYXJhZHltLmlkL3R5cGVzLzZmVEVnRlVMdjItRW1wbG95ZWVCYWRnZSIsImNuZiI6eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbmdpT2lKQlJWaHdTSGt4TUVkb2RGZG9iRlpRVG0xeVJuTmllWFJmZDBSelVWODNjVE5rYWs1dWNtaDZhbDgwSWl3aWVTSTZJa1JIVkVGRFQwRkJibEZVWlhCaFJEUXdaM2xIT1Zwc0xXOUVhRTlzZGpOVlFteFVkSGhKWlhJMVpXOGlMQ0pqY25ZaU9pSlFMVEkxTmlKOSMwIn0sImlzcyI6ImRpZDp3ZWI6bWV0YWRhdGEucGFyYWR5bS5pZDowYzIwNzI3Ny02NjU2LTQ2MzItOWQyOC03MGYzNGRkZTllYTIiLCJpYXQiOjE3Mjk1Njc3MTcsIl9zZCI6WyJHY2NVSjNGdW1pb3I0bDdqZnpXSmFvUjBTSzNaQU5penQ4ZW1HOTBVLWQ0IiwiVWhCUEp5VWdmTFFnX1JUUnJLM2xNN25ldWhZT3JlYVNBNTlVNTJQUnNZUSIsInYtTHhvdDhWczBzWi1VMGRZZ0dZMFppem9BYVc4eWt3NC1kaVhDODhIeEkiLCJ2Y1pwZW5nUkhsV2hEUUhhczVvOW1TNUM3VDRFUU92ekVWcC05MHF2OHVVIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.sceont7otHdLLwAMNQkkppakbZiX0YVWgxTejugsZUkaOFtIYVM8pNLZTs-Oi_AIRvwIZ6uuuMK8TOmp6QSfAg~WyI2NDU1NTI0ODUwOTEwNzMzMDk3MzI1NDUiLCJpc19hZG1pbiIsdHJ1ZV0~WyIxMDc0NzQzNzg3OTY1NzI0ODkxNDkwMDciLCJsYXN0X25hbWUiLCJTdHJpbmcgdmFsdWUiXQ~WyI2OTg2MDU4NTY2NDc3MjQwNTE0MTI3MTMiLCJmaXJzdF9uYW1lIiwiU3RyaW5nIHZhbHVlIl0~WyIyODIyODI0Nzg0ODQ4MTYxNjA4NDM0NTkiLCJlbXBsb3llZV9pZCIsIlN0cmluZyB2YWx1ZSJd~', + c_nonce: '1f476b83-00fc-44e6-8cfa-c52c5df12d08', + c_nonce_expires_in: 300, + }, + holderPrivateKeyJwk: { + kty: 'EC', + x: 'AEXpHy10GhtWhlVPNmrFsbyt_wDsQ_7q3djNnrhzj_4', + y: 'DGTACOAAnQTepaD40gyG9Zl-oDhOlv3UBlTtxIer5eo', + crv: 'P-256', + d: 'C75pQj72AAl6SCsBW8AKTKxqLGk2Fw7NutIpWZ-xjvE', + }, +} + export const paradymDraft11 = { credentialOffer: 'https://paradym.id/invitation?credential_offer_uri=https%3A%2F%2Fparadym.id%2Finvitation%2Fdraft-11-issuer%2Foffers%2Fb99db8f1-4fa2-4b27-8dc7-ecf81478eb9b%3Fraw%3Dtrue', diff --git a/packages/oid4vci/src/formats/proof-type/jwt/v-jwt-proof-type.ts b/packages/oid4vci/src/formats/proof-type/jwt/v-jwt-proof-type.ts index 82d931e..6a235da 100644 --- a/packages/oid4vci/src/formats/proof-type/jwt/v-jwt-proof-type.ts +++ b/packages/oid4vci/src/formats/proof-type/jwt/v-jwt-proof-type.ts @@ -22,7 +22,10 @@ export const vCredentialRequestJwtProofTypeHeader = v.pipe( ({ kid, jwk }) => jwk === undefined || kid === undefined, `Both 'jwk' and 'kid' are defined. Only one is allowed` ), - v.check(({ trust_chain, kid }) => !trust_chain || !kid, `When 'trust_chain' is provided, 'kid' is required`) + v.check( + ({ trust_chain, kid }) => !trust_chain || kid !== undefined, + `When 'trust_chain' is provided, 'kid' is required` + ) ) export type CredentialRequestJwtProofTypeHeader = v.InferOutput diff --git a/packages/oid4vci/src/metadata/credential-issuer/credential-issuer-metadata.ts b/packages/oid4vci/src/metadata/credential-issuer/credential-issuer-metadata.ts index 97692fc..7abb837 100644 --- a/packages/oid4vci/src/metadata/credential-issuer/credential-issuer-metadata.ts +++ b/packages/oid4vci/src/metadata/credential-issuer/credential-issuer-metadata.ts @@ -27,8 +27,6 @@ export async function fetchCredentialIssuerMetadata( credentialIssuer: string, options: FetchCredentialIssuerMetadataOptions ): Promise<{ credentialIssuerMetadata: CredentialIssuerMetadata; originalDraftVersion: Oid4vciDraftVersion } | null> { - // TODO: What should we do when it has the property trust_chain? - let result: v.InferOutput | null = null const entityConfiguration = await fetchEntityConfiguration({ @@ -42,7 +40,7 @@ export async function fetchCredentialIssuerMetadata( { alg: jwk.alg, method: 'jwk', - publicJwk: jwk as Jwk, // TODO: Check why this type is not correct + publicJwk: jwk as Jwk, }, { header: header as JwtHeader,