From a37ef06d38fdc7a6d5acc372cd2da8935b4c414e Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Thu, 16 Nov 2023 20:14:02 +0700 Subject: [PATCH] feat: add sd-jwt support Signed-off-by: Timo Glastra --- .../lib/__tests__/issuerCallback.spec.ts | 18 +++-- .../client/lib/AuthorizationDetailsBuilder.ts | 4 +- .../client/lib/CredentialRequestClient.ts | 79 ++++++++++--------- .../lib/CredentialRequestClientBuilder.ts | 3 +- packages/client/lib/OpenID4VCIClient.ts | 20 +++-- .../__tests__/CredentialRequestClient.spec.ts | 19 +++-- .../lib/__tests__/data/VciDataFixtures.ts | 25 +++--- .../lib/functions/CredentialOfferUtil.ts | 27 +++++++ .../lib/functions/CredentialRequestUtil.ts | 34 +++++++- packages/common/lib/functions/FormatUtils.ts | 52 ++++++++++++ .../lib/functions/IssuerMetadataUtils.ts | 23 +++++- packages/common/lib/functions/index.ts | 1 + .../common/lib/types/Authorization.types.ts | 33 ++++++-- packages/common/lib/types/Generic.types.ts | 75 ++++++++++++++---- packages/common/lib/types/v1_0_08.types.ts | 9 ++- packages/common/lib/types/v1_0_11.types.ts | 26 ++---- .../lib/__tests__/IssuerTokenServer.spec.ts | 23 +++--- packages/issuer/lib/VcIssuer.ts | 4 +- .../issuer/lib/__tests__/VcIssuer.spec.ts | 18 +++-- .../CredentialSupportedBuilderV1_11.ts | 26 +++++- packages/tsconfig.json | 2 +- 21 files changed, 377 insertions(+), 144 deletions(-) create mode 100644 packages/common/lib/functions/FormatUtils.ts diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index e550aaea..16ae66d2 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -4,7 +4,6 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess import { Alg, CNonceState, - CredentialOfferLdpVcV1_0_11, CredentialSupported, IssuerCredentialSubjectDisplay, IssueStatus, @@ -118,11 +117,16 @@ describe('issuerCallback', () => { credentialOffer: { credential_offer: { credential_issuer: 'did:key:test', - credential_definition: { - types: ['VerifiableCredential'], - '@context': ['https://www.w3.org/2018/credentials/v1'], - credentialSubject: {}, - }, + credentials: [ + { + format: 'ldp_vc', + credential_definition: { + types: ['VerifiableCredential'], + '@context': ['https://www.w3.org/2018/credentials/v1'], + credentialSubject: {}, + }, + }, + ], grants: { authorization_code: { issuer_state: 'test_code' }, 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { @@ -130,7 +134,7 @@ describe('issuerCallback', () => { user_pin_required: true, }, }, - } as CredentialOfferLdpVcV1_0_11, + }, }, }) diff --git a/packages/client/lib/AuthorizationDetailsBuilder.ts b/packages/client/lib/AuthorizationDetailsBuilder.ts index b052722b..9b7de3d1 100644 --- a/packages/client/lib/AuthorizationDetailsBuilder.ts +++ b/packages/client/lib/AuthorizationDetailsBuilder.ts @@ -1,8 +1,8 @@ -import { AuthorizationDetailsJwtVcJson, OID4VCICredentialFormat } from '@sphereon/oid4vci-common'; +import { AuthorizationDetails, AuthorizationDetailsJwtVcJson, OID4VCICredentialFormat } from '@sphereon/oid4vci-common'; //todo: refactor this builder to be able to create ldp details as well export class AuthorizationDetailsBuilder { - private readonly authorizationDetails: Partial; + private readonly authorizationDetails: Partial>; constructor() { this.authorizationDetails = {}; diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 055c8006..8bcdfb89 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -1,6 +1,7 @@ import { - CredentialRequestV1_0_08, CredentialResponse, + getCredentialRequestForVersion, + getUniformFormat, OID4VCICredentialFormat, OpenId4VCIVersion, OpenIDResponse, @@ -53,24 +54,7 @@ export class CredentialRequestClient { } public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise> { - let request: CredentialRequestV1_0_08 | UniformCredentialRequest = uniformRequest; - if (!this.isV11OrHigher()) { - let format: string = uniformRequest.format; - if (format === 'jwt_vc_json') { - format = 'jwt_vc'; - } else if (format === 'jwt_vc_json-ld') { - format = 'ldp_vc'; - } - - request = { - format, - proof: uniformRequest.proof, - type: - 'types' in uniformRequest - ? uniformRequest.types.filter((t) => t !== 'VerifiableCredential')[0] - : uniformRequest.credential_definition.types[0], - } as CredentialRequestV1_0_08; - } + const request = getCredentialRequestForVersion(uniformRequest, this.version()); const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint; if (!isValidURL(credentialEndpoint)) { debug(`Invalid credential endpoint: ${credentialEndpoint}`); @@ -92,20 +76,10 @@ export class CredentialRequestClient { const { proofInput } = opts; const formatSelection = opts.format ?? this.credentialRequestOpts.format; - let format: OID4VCICredentialFormat = formatSelection as OID4VCICredentialFormat; - if (opts.version < OpenId4VCIVersion.VER_1_0_11) { - if (formatSelection === 'jwt_vc' || formatSelection === 'jwt') { - format = 'jwt_vc_json'; - } else if (formatSelection === 'ldp_vc' || formatSelection === 'ldp') { - format = 'jwt_vc_json-ld'; - } - } - - if (!format) { + if (!formatSelection) { throw Error(`Format of credential to be issued is missing`); - } else if (format !== 'jwt_vc_json-ld' && format !== 'jwt_vc_json' && format !== 'ldp_vc') { - throw Error(`Invalid format of credential to be issued: ${format}`); } + const format = getUniformFormat(formatSelection); const typesSelection = opts?.credentialTypes && (typeof opts.credentialTypes === 'string' || opts.credentialTypes.length > 0) ? opts.credentialTypes @@ -113,7 +87,9 @@ export class CredentialRequestClient { const types = Array.isArray(typesSelection) ? typesSelection : [typesSelection]; if (types.length === 0) { throw Error(`Credential type(s) need to be provided`); - } else if (!this.isV11OrHigher() && types.length !== 1) { + } + // FIXME: this is mixing up the type (as id) from v8/v9 and the types (from the vc.type) from v11 + else if (!this.isV11OrHigher() && types.length !== 1) { throw Error('Only a single credential type is supported for V8/V9'); } @@ -121,16 +97,45 @@ export class CredentialRequestClient { 'proof_type' in proofInput ? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build() : await proofInput.build(); - return { - types, - format, - proof, - } as UniformCredentialRequest; + + // TODO: we should move format specific logic + if (format === 'jwt_vc_json') { + return { + types, + format, + proof, + }; + } else if (format === 'jwt_vc_json-ld' || format === 'ldp_vc') { + return { + format, + proof, + credential_definition: { + types, + // FIXME: this was not included in the original code, but it is required + '@context': [], + }, + }; + } else if (format === 'vc+sd-jwt') { + if (types.length > 1) { + throw Error(`Only a single credential type is supported for ${format}`); + } + + return { + format, + proof, + credential_definition: { + vct: types[0], + }, + }; + } + + throw new Error(`Unsupported format: ${format}`); } private version(): OpenId4VCIVersion { return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11; } + private isV11OrHigher(): boolean { return this.version() >= OpenId4VCIVersion.VER_1_0_11; } diff --git a/packages/client/lib/CredentialRequestClientBuilder.ts b/packages/client/lib/CredentialRequestClientBuilder.ts index 1229a054..c3a0498e 100644 --- a/packages/client/lib/CredentialRequestClientBuilder.ts +++ b/packages/client/lib/CredentialRequestClientBuilder.ts @@ -6,6 +6,7 @@ import { determineSpecVersionFromOffer, EndpointMetadata, getIssuerFromCredentialOfferPayload, + getTypesFromOffer, OID4VCICredentialFormat, OpenId4VCIVersion, UniformCredentialOfferRequest, @@ -46,7 +47,7 @@ export class CredentialRequestClientBuilder { builder.withCredentialType((request.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type); } else { // todo: look whether this is correct - builder.withCredentialType(request.credential_offer.credentials.flatMap((c) => (typeof c === 'string' ? c : c.types))); + builder.withCredentialType(getTypesFromOffer(request.credential_offer)); } return builder; diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 90aa0955..227eb835 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -15,7 +15,7 @@ import { PushedAuthorizationResponse, ResponseType, } from '@sphereon/oid4vci-common'; -import { getSupportedCredentials } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils'; +import { getSupportedCredentials, getTypesFromCredentialSupported } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils'; import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types'; import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; @@ -312,12 +312,10 @@ export class OpenID4VCIClient { let typeSupported = false; metadata.credentials_supported.forEach((supportedCredential) => { - if (!supportedCredential.types || supportedCredential.types.length === 0) { - throw Error('types is required in the credentials supported'); - } + const subTypes = getTypesFromCredentialSupported(supportedCredential); if ( - supportedCredential.types.sort().every((t, i) => types[i] === t) || - (types.length === 1 && (types[0] === supportedCredential.id || supportedCredential.types.includes(types[0]))) + subTypes.sort().every((t, i) => types[i] === t) || + (types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0]))) ) { typeSupported = true; } @@ -397,7 +395,15 @@ export class OpenID4VCIClient { return result; } else { return this.credentialOffer.credential_offer.credentials.map((c) => { - return typeof c === 'string' ? [c] : c.types; + if (typeof c === 'string') { + return [c]; + } else if ('types' in c) { + return c.types; + } else if ('vct' in c.credential_definition) { + return [c.credential_definition.vct]; + } else { + return c.credential_definition.types; + } }); } } diff --git a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts index 8b112a85..c908a5a9 100644 --- a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts @@ -3,6 +3,7 @@ import { KeyObject } from 'crypto'; import { Alg, EndpointMetadata, + getCredentialRequestForVersion, getIssuerFromCredentialOfferPayload, Jwt, OpenId4VCIVersion, @@ -149,9 +150,7 @@ describe('Credential Request Client ', () => { .withKid(kid) .withClientId('sphereon:wallet') .build(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json-ld', types: ['random'], proof })).rejects.toThrow( + await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json', types: ['random'], proof })).rejects.toThrow( Error(URL_NOT_VALID), ); }); @@ -194,10 +193,11 @@ describe('Credential Request Client with different issuers ', () => { jwt: getMockData('spruce')?.credential.request.proof.jwt as string, }, credentialTypes: ['OpenBadgeCredential'], - format: 'jwt_vc_json-ld', + format: 'jwt_vc', version: OpenId4VCIVersion.VER_1_0_08, }); - expect(credentialRequest).toEqual(getMockData('spruce')?.credential.request); + const draft8CredentialRequest = getCredentialRequestForVersion(credentialRequest, OpenId4VCIVersion.VER_1_0_08); + expect(draft8CredentialRequest).toEqual(getMockData('spruce')?.credential.request); }); it('should create correct CredentialRequest for Walt', async () => { @@ -264,7 +264,8 @@ describe('Credential Request Client with different issuers ', () => { format: 'ldp_vc', version: OpenId4VCIVersion.VER_1_0_08, }); - expect(credentialOffer).toEqual(getMockData('mattr')?.credential.request); + const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08); + expect(credentialRequest).toEqual(getMockData('mattr')?.credential.request); }); it('should create correct CredentialRequest for diwala', async () => { @@ -286,6 +287,10 @@ describe('Credential Request Client with different issuers ', () => { format: 'ldp_vc', version: OpenId4VCIVersion.VER_1_0_08, }); - expect(credentialOffer).toEqual(getMockData('diwala')?.credential.request); + + // createCredentialRequest returns uniform format in draft 11 + const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08); + + expect(credentialRequest).toEqual(getMockData('diwala')?.credential.request); }); }); diff --git a/packages/client/lib/__tests__/data/VciDataFixtures.ts b/packages/client/lib/__tests__/data/VciDataFixtures.ts index f7f1091d..afce2671 100644 --- a/packages/client/lib/__tests__/data/VciDataFixtures.ts +++ b/packages/client/lib/__tests__/data/VciDataFixtures.ts @@ -1,4 +1,4 @@ -import { CredentialSupportedBrief, IssuerCredentialSubjectDisplay, IssuerMetadataV1_0_08 } from '@sphereon/oid4vci-common'; +import { CredentialSupportedFormatV1_0_08, IssuerCredentialSubjectDisplay, IssuerMetadataV1_0_08 } from '@sphereon/oid4vci-common'; import { ICredentialStatus, W3CVerifiableCredential } from '@sphereon/ssi-types'; export function getMockData(issuerName: string): IssuerMockData | null { @@ -42,7 +42,8 @@ export interface IssuerMockData { url: string; deeplink: string; request: { - types: [string]; + types?: [string]; + type?: string; format: 'jwt_vc' | 'ldp_vc' | 'jwt_vc_json-ld' | string; proof: { proof_type: 'jwt' | string; @@ -110,8 +111,8 @@ const mockData: VciMockDataStructure = { deeplink: 'openid-initiate-issuance://?issuer=https%3A%2F%2Fngi%2Doidc4vci%2Dtest%2Espruceid%2Exyz&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJFUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOlsiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJleHAiOiIyMDIzLTA0LTIwVDA5OjA0OjM2WiIsIm5vbmNlIjoibWFibmVpT0VSZVB3V3BuRFFweEt3UnRsVVRFRlhGUEwifQ.qOZRPN8sTv_knhp7WaWte2-aDULaPZX--2i9unF6QDQNUllqDhvxgIHMDCYHCV8O2_Gj-T2x1J84fDMajE3asg&user_pin_required=false', request: { - types: ['OpenBadgeCredential'], - format: 'jwt_vc_json-ld', + type: 'OpenBadgeCredential', + format: 'jwt_vc', proof: { proof_type: 'jwt', jwt: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOa3NpTENKMWMyVWlPaUp6YVdjaUxDSnJkSGtpT2lKRlF5SXNJbU55ZGlJNkluTmxZM0F5TlRack1TSXNJbmdpT2lKclpuVmpTa0V0VEhKck9VWjBPRmx5TFVkMlQzSmpia3N3YjNkc2RqUlhNblUwU3pJeFNHZHZTVlIzSWl3aWVTSTZJalozY0ZCUE1rOUNRVXBTU0ZFMVRXdEtXVlJaV0dsQlJFUXdOMU5OTlV0amVXcDNYMkUzVUUxWmVGa2lmUSMwIn0.eyJhdWQiOiJodHRwczovL25naS1vaWRjNHZjaS10ZXN0LnNwcnVjZWlkLnh5eiIsImlhdCI6MTY4MTkxMTA2MC45NDIsImV4cCI6MTY4MTkxMTcyMC45NDIsImlzcyI6InNwaGVyZW9uOnNzaS13YWxsZXQiLCJqdGkiOiJhNjA4MzMxZi02ZmE0LTQ0ZjAtYWNkZWY5NmFjMjdmNmQ3MCJ9.NwF3_41gwnlIdd_6Uk9CczeQHzIQt6UcvTT5Cxv72j9S1vNwiY9annA2kLsjsTiR5-WMBdUhJCO7wYCtZ15mxw', @@ -514,7 +515,7 @@ const mockData: VciMockDataStructure = { types: ['PermanentResidentCard'], binding_methods_supported: ['did'], cryptographic_suites_supported: ['Ed25519Signature2018'], - } as CredentialSupportedBrief, + } as CredentialSupportedFormatV1_0_08, }, }, AcademicAward: { @@ -525,7 +526,7 @@ const mockData: VciMockDataStructure = { types: ['AcademicAward'], binding_methods_supported: ['did'], cryptographic_suites_supported: ['Ed25519Signature2018'], - } as CredentialSupportedBrief, + } as CredentialSupportedFormatV1_0_08, }, }, LearnerProfile: { @@ -536,7 +537,7 @@ const mockData: VciMockDataStructure = { types: ['LearnerProfile'], binding_methods_supported: ['did'], cryptographic_suites_supported: ['Ed25519Signature2018'], - } as CredentialSupportedBrief, + } as CredentialSupportedFormatV1_0_08, }, }, OpenBadgeCredential: { @@ -547,7 +548,7 @@ const mockData: VciMockDataStructure = { types: ['OpenBadgeCredential'], binding_methods_supported: ['did'], cryptographic_suites_supported: ['Ed25519Signature2018'], - } as CredentialSupportedBrief, + } as CredentialSupportedFormatV1_0_08, }, }, }, @@ -573,8 +574,8 @@ const mockData: VciMockDataStructure = { 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=g0UCOj6RAN5AwHU6gczm_GzB4_lH6GW39Z0Dl2DOOiO', url: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential', request: { - types: ['OpenBadgeCredential'], - format: 'jwt_vc_json-ld', + type: 'OpenBadgeCredential', + format: 'ldp_vc', proof: { proof_type: 'jwt', jwt: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3AxM3N6QUFMVFN0cDV1OGtMcnl5YW5vYWtrVWtFUGZXazdvOHY3dms0RW1KI3o2TWtwMTNzekFBTFRTdHA1dThrTHJ5eWFub2Fra1VrRVBmV2s3bzh2N3ZrNEVtSiJ9.eyJhdWQiOiJodHRwczovL2xhdW5jaHBhZC5tYXR0cmxhYnMuY29tIiwiaWF0IjoxNjgxOTE0NDgyLjUxOSwiZXhwIjoxNjgxOTE1MTQyLjUxOSwiaXNzIjoic3BoZXJlb246c3NpLXdhbGxldCIsImp0aSI6ImI5NDY1ZGE5LTY4OGYtNDdjNi04MjUwNDA0ZGNiOWI5Y2E5In0.uQ8ewOfIjy_1p_Gk6PjeEWccBJnjOca1pwbTWiCAFMQX9wlIsfeUdGtXUoHjH5_PQtpwytodx7WU456_CT9iBQ', @@ -687,8 +688,8 @@ const mockData: VciMockDataStructure = { 'openid-initiate-issuance://?issuer=https://oidc4vc.diwala.io&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJIUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZXhwIjoxNjgxOTg0NDY3fQ.fEAHKz2nuWfiYHw406iNxr-81pWkNkbi31bWsYSf6Ng', url: 'https://oidc4vc.diwala.io/credential', request: { - types: ['OpenBadgeCredential'], - format: 'jwt_vc_json-ld', + type: 'OpenBadgeCredential', + format: 'ldp_vc', proof: { proof_type: 'jwt', jwt: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3AxM3N6QUFMVFN0cDV1OGtMcnl5YW5vYWtrVWtFUGZXazdvOHY3dms0RW1KI3o2TWtwMTNzekFBTFRTdHA1dThrTHJ5eWFub2Fra1VrRVBmV2s3bzh2N3ZrNEVtSiJ9.eyJhdWQiOiJodHRwczovL29pZGM0dmMuZGl3YWxhLmlvIiwiaWF0IjoxNjgxOTE1MDk1LjIwMiwiZXhwIjoxNjgxOTE1NzU1LjIwMiwiaXNzIjoic3BoZXJlb246c3NpLXdhbGxldCIsImp0aSI6IjYxN2MwM2EzLTM3MTUtNGJlMy1hYjkxNzM4MTlmYzYxNTYzIn0.KA-cHjecaYp9FSaWHkz5cqtNyhBIVT_0I7cJnpHn03T4UWFvdhjhn8Hpe-BU247enFyWOWJ6v3NQZyZgle7xBA', diff --git a/packages/common/lib/functions/CredentialOfferUtil.ts b/packages/common/lib/functions/CredentialOfferUtil.ts index db4f4502..7f2428f1 100644 --- a/packages/common/lib/functions/CredentialOfferUtil.ts +++ b/packages/common/lib/functions/CredentialOfferUtil.ts @@ -340,3 +340,30 @@ function recordVersion(currentVersion: OpenId4VCIVersion, matchingVersion: OpenI `Invalid param. Some keys have been used from version: ${currentVersion} version while '${key}' is used from version: ${matchingVersion}`, ); } + +export function getTypesFromOffer(credentialOffer: UniformCredentialOfferPayload, opts?: { filterVerifiableCredential: boolean }) { + const types = credentialOffer.credentials.reduce((prev, curr) => { + // FIXME returning the string value is wrong (as it's an id), but just matching the current behavior of this library + // The credential_type (from draft 8) and the actual 'type' value in a VC (from draft 11) are mixed up + // Fix for this here: https://github.com/Sphereon-Opensource/OID4VCI/pull/54 + if (typeof curr === 'string') { + return [...prev, curr]; + } else if (curr.format === 'jwt_vc_json-ld' || curr.format === 'ldp_vc') { + return [...prev, ...curr.credential_definition.types]; + } else if (curr.format === 'jwt_vc_json') { + return [...prev, ...curr.types]; + } else if (curr.format === 'vc+sd-jwt') { + return [...prev, curr.credential_definition.vct]; + } + + return prev; + }, []); + + if (!types || types.length === 0) { + throw Error('Could not deduce types from credential offer'); + } + if (opts?.filterVerifiableCredential) { + return types.filter((type) => type !== 'VerifiableCredential'); + } + return types; +} diff --git a/packages/common/lib/functions/CredentialRequestUtil.ts b/packages/common/lib/functions/CredentialRequestUtil.ts index 48dcb34f..66ee9fe6 100644 --- a/packages/common/lib/functions/CredentialRequestUtil.ts +++ b/packages/common/lib/functions/CredentialRequestUtil.ts @@ -1,7 +1,17 @@ -import { CredentialRequestV1_0_11 } from '../types'; +import { CredentialRequestV1_0_08, OpenId4VCIVersion, UniformCredentialRequest } from '../types'; + +import { getFormatForVersion } from './FormatUtils'; + +export function getTypesFromRequest(credentialRequest: UniformCredentialRequest, opts?: { filterVerifiableCredential: boolean }) { + let types: string[] = []; + if (credentialRequest.format === 'jwt_vc_json') { + types = credentialRequest.types; + } else if (credentialRequest.format === 'jwt_vc_json-ld' || credentialRequest.format === 'ldp_vc') { + types = credentialRequest.credential_definition.types; + } else if (credentialRequest.format === 'vc+sd-jwt') { + types = [credentialRequest.credential_definition.vct]; + } -export function getTypesFromRequest(credentialRequest: CredentialRequestV1_0_11, opts?: { filterVerifiableCredential: boolean }) { - const types = 'types' in credentialRequest ? credentialRequest.types : credentialRequest.credential_definition.types; if (!types || types.length === 0) { throw Error('Could not deduce types from credential request'); } @@ -10,3 +20,21 @@ export function getTypesFromRequest(credentialRequest: CredentialRequestV1_0_11, } return types; } + +export function getCredentialRequestForVersion( + credentialRequest: UniformCredentialRequest, + version: OpenId4VCIVersion, +): UniformCredentialRequest | CredentialRequestV1_0_08 { + if (version === OpenId4VCIVersion.VER_1_0_08) { + const draft8Format = getFormatForVersion(credentialRequest.format, version); + const types = getTypesFromRequest(credentialRequest, { filterVerifiableCredential: true }); + + return { + format: draft8Format, + proof: credentialRequest.proof, + type: types[0], + } satisfies CredentialRequestV1_0_08; + } + + return credentialRequest; +} diff --git a/packages/common/lib/functions/FormatUtils.ts b/packages/common/lib/functions/FormatUtils.ts new file mode 100644 index 00000000..2eda8449 --- /dev/null +++ b/packages/common/lib/functions/FormatUtils.ts @@ -0,0 +1,52 @@ +import { CredentialFormat } from '@sphereon/ssi-types'; + +import { OID4VCICredentialFormat, OpenId4VCIVersion } from '../types'; + +export function isFormat( + formatObject: T, + format: Format, +): formatObject is T & { format: Format } { + return formatObject.format === format; +} + +export function isNotFormat( + formatObject: T, + format: Format, +): formatObject is T & { format: Exclude } { + return formatObject.format !== format; +} + +const isUniformFormat = (format: string): format is OID4VCICredentialFormat => { + return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc', 'vc+sd-jwt'].includes(format); +}; + +export function getUniformFormat(format: string | OID4VCICredentialFormat | CredentialFormat): OID4VCICredentialFormat { + // Already valid format + if (isUniformFormat(format)) { + return format; + } + + // Older formats + if (format === 'jwt_vc' || format === 'jwt') { + return 'jwt_vc_json'; + } + if (format === 'ldp_vc' || format === 'ldp') { + return 'ldp_vc'; + } + + throw new Error(`Invalid format: ${format}`); +} + +export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { + const uniformFormat = isUniformFormat(format) ? format : getUniformFormat(format); + + if (version === OpenId4VCIVersion.VER_1_0_08) { + if (uniformFormat === 'jwt_vc_json') { + return 'jwt_vc' as const; + } else if (uniformFormat === 'ldp_vc' || uniformFormat === 'jwt_vc_json-ld') { + return 'ldp_vc' as const; + } + } + + return uniformFormat; +} diff --git a/packages/common/lib/functions/IssuerMetadataUtils.ts b/packages/common/lib/functions/IssuerMetadataUtils.ts index 379e12a9..40eb85d6 100644 --- a/packages/common/lib/functions/IssuerMetadataUtils.ts +++ b/packages/common/lib/functions/IssuerMetadataUtils.ts @@ -73,7 +73,7 @@ export function getSupportedCredential(opts?: { if ((opts?.types && typeof opts?.types === 'string') || opts?.types?.length === 1) { const types = Array.isArray(opts.types) ? opts.types[0] : opts.types; const supported = credentialsSupported.filter( - (sup) => sup.id === types || (initiationTypes && arrayEqualsIgnoreOrder(sup.types, initiationTypes)), + (sup) => sup.id === types || (initiationTypes && arrayEqualsIgnoreOrder(getTypesFromCredentialSupported(sup), initiationTypes)), ); if (supported) { credentialSupportedOverlap.push(...supported); @@ -86,7 +86,7 @@ export function getSupportedCredential(opts?: { initiationTypes.push('VerifiableCredential'); } const supported = credentialsSupported.filter((sup) => { - const supTypes = sup.types; + const supTypes = getTypesFromCredentialSupported(sup); if (!supTypes.includes('VerifiableCredential')) { supTypes.push('VerifiableCredential'); } @@ -99,6 +99,23 @@ export function getSupportedCredential(opts?: { return credentialSupportedOverlap; } +export function getTypesFromCredentialSupported(credentialSupported: CredentialSupported, opts?: { filterVerifiableCredential: boolean }) { + let types: string[] = []; + if (credentialSupported.format === 'jwt_vc_json' || credentialSupported.format === 'jwt_vc_json-ld' || credentialSupported.format === 'ldp_vc') { + types = credentialSupported.types; + } else if (credentialSupported.format === 'vc+sd-jwt') { + types = [credentialSupported.credential_definition.vct]; + } + + if (!types || types.length === 0) { + throw Error('Could not deduce types from credential supported'); + } + if (opts?.filterVerifiableCredential) { + return types.filter((type) => type !== 'VerifiableCredential'); + } + return types; +} + function arrayEqualsIgnoreOrder(a: string[], b: string[]) { if (a.length !== b.length) return false; const uniqueValues = new Set([...a, ...b]); @@ -127,7 +144,7 @@ export function credentialSupportedV8ToV11(key: string, supportedV8: CredentialS } let credentialSupport: Partial = {}; credentialSupport = { - format, + format: format as OID4VCICredentialFormat, display: supportedV8.display, ...credentialSupportBrief, credentialSubject: supportedV8.claims, diff --git a/packages/common/lib/functions/index.ts b/packages/common/lib/functions/index.ts index 2f293989..f192cab0 100644 --- a/packages/common/lib/functions/index.ts +++ b/packages/common/lib/functions/index.ts @@ -2,3 +2,4 @@ export * from './CredentialRequestUtil'; export * from './CredentialOfferUtil'; export * from './Encoding'; export * from './TypeConversionUtils'; +export * from './FormatUtils'; diff --git a/packages/common/lib/types/Authorization.types.ts b/packages/common/lib/types/Authorization.types.ts index 5185e2ee..0276d510 100644 --- a/packages/common/lib/types/Authorization.types.ts +++ b/packages/common/lib/types/Authorization.types.ts @@ -1,5 +1,12 @@ import { CredentialOfferPayload, UniformCredentialOffer } from './CredentialIssuance.types'; -import { ErrorResponse, IssuerCredentialDefinition, IssuerCredentialSubject, OID4VCICredentialFormat, PRE_AUTH_CODE_LITERAL } from './Generic.types'; +import { + ErrorResponse, + IssuerCredentialSubject, + JsonLdIssuerCredentialDefinition, + OID4VCICredentialFormat, + PRE_AUTH_CODE_LITERAL, + SdJwtVcCredentialDefinition, +} from './Generic.types'; import { EndpointMetadata } from './ServerMetadata'; export interface CommonAuthorizationRequest { @@ -64,9 +71,9 @@ export interface CommonAuthorizationRequest { /** * string type added for conformity with our previous code in the client */ -export type AuthorizationDetails = AuthorizationDetailsJwtVcJson | AuthorizationRequestJwtVcJsonLdAndLdpVc | string; +export type AuthorizationDetails = AuthorizationDetailsJwtVcJson | AuthorizationDetailsJwtVcJsonLdAndLdpVc | AuthorizationDetailsSdJwtVc | string; -export type AuthorizationRequest = AuthorizationRequestJwtVcJson | AuthorizationDetailsJwtVcJsonLdAndLdpVc; +export type AuthorizationRequest = AuthorizationRequestJwtVcJson | AuthorizationRequestJwtVcJsonLdAndLdpVc | AuthorizationRequestSdJwtVc; export interface AuthorizationRequestJwtVcJson extends CommonAuthorizationRequest { authorization_details?: AuthorizationDetailsJwtVcJson[]; @@ -76,6 +83,10 @@ export interface AuthorizationRequestJwtVcJsonLdAndLdpVc extends CommonAuthoriza authorization_details?: AuthorizationDetailsJwtVcJsonLdAndLdpVc[]; } +export interface AuthorizationRequestSdJwtVc extends CommonAuthorizationRequest { + authorization_details?: AuthorizationDetailsSdJwtVc[]; +} + export interface CommonAuthorizationDetails { /** * REQUIRED. JSON string that determines the authorization details type. @@ -94,12 +105,14 @@ export interface CommonAuthorizationDetails { * the authorization detail's locations common data field MUST be set to the Credential Issuer Identifier value. */ locations?: string[]; - types: string[]; // This claim contains the type values the Wallet requests authorization for at the issuer. + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } export interface AuthorizationDetailsJwtVcJson extends CommonAuthorizationDetails { + format: 'jwt_vc_json'; + /** * A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. * The value MAY be a dictionary, which allows to represent the full (potentially deeply nested) structure of the @@ -107,9 +120,13 @@ export interface AuthorizationDetailsJwtVcJson extends CommonAuthorizationDetail * credential to be issued. */ credentialSubject?: IssuerCredentialSubject; + + types: string[]; // This claim contains the type values the Wallet requests authorization for at the issuer. } export interface AuthorizationDetailsJwtVcJsonLdAndLdpVc extends CommonAuthorizationDetails { + format: 'ldp_vc' | 'jwt_vc_json-ld'; + /** * REQUIRED. JSON object containing (and isolating) the detailed description of the credential type. * This object MUST be processed using full JSON-LD processing. It consists of the following sub-claims: @@ -117,7 +134,13 @@ export interface AuthorizationDetailsJwtVcJsonLdAndLdpVc extends CommonAuthoriza * - types: REQUIRED. JSON array as defined in Appendix E.1.3.2. * This claim contains the type values the Wallet shall request in the subsequent Credential Request */ - credential_definition: IssuerCredentialDefinition; + credential_definition: JsonLdIssuerCredentialDefinition; +} + +export interface AuthorizationDetailsSdJwtVc extends CommonAuthorizationDetails { + format: 'vc+sd-jwt'; + + credential_definition: SdJwtVcCredentialDefinition; } export enum GrantTypes { diff --git a/packages/common/lib/types/Generic.types.ts b/packages/common/lib/types/Generic.types.ts index 4266cc09..46187a29 100644 --- a/packages/common/lib/types/Generic.types.ts +++ b/packages/common/lib/types/Generic.types.ts @@ -15,7 +15,7 @@ export interface ImageInfo { [key: string]: unknown; } -export type OID4VCICredentialFormat = 'jwt_vc_json' | 'jwt_vc_json-ld' | 'ldp_vc' /*| 'mso_mdoc'*/; // we do not support mdocs at this point +export type OID4VCICredentialFormat = 'jwt_vc_json' | 'jwt_vc_json-ld' | 'ldp_vc' | 'vc+sd-jwt' /*| 'mso_mdoc'*/; // we do not support mdocs at this point export interface NameAndLocale { name?: string; // REQUIRED. String value of a display name for the Credential. @@ -62,8 +62,6 @@ export interface CredentialIssuerMetadata extends CredentialIssuerMetadataOpts, } export interface CredentialSupportedBrief { - name?: string; // fixme: Probably should not be here, is part of the display object - types: string[]; // REQUIRED. JSON array designating the types a certain credential type supports cryptographic_binding_methods_supported?: string[]; // OPTIONAL. Array of case sensitive strings that identify how the Credential is bound to the identifier of the End-User who possesses the Credential cryptographic_suites_supported?: string[]; // OPTIONAL. Array of case sensitive strings that identify the cryptographic suites that are supported for the cryptographic_binding_methods_supported } @@ -75,25 +73,62 @@ export type CommonCredentialSupported = CredentialSupportedBrief & { /** * following properties are non-mso_mdoc specific and we might wanna rethink them when we're going to support mso_mdoc */ - credentialSubject?: IssuerCredentialSubject; // OPTIONAL. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value MAY be a dictionary, which allows to represent the full (potentially deeply nested) structure of the verifiable credential to be issued. - order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. }; export interface CredentialSupportedJwtVcJsonLdAndLdpVc extends CommonCredentialSupported { + types: string[]; // REQUIRED. JSON array designating the types a certain credential type supports '@context': ICredentialContextType[]; // REQUIRED. JSON array as defined in [VC_DATA], Section 4.1. + credentialSubject?: IssuerCredentialSubject; // OPTIONAL. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value MAY be a dictionary, which allows to represent the full (potentially deeply nested) structure of the verifiable credential to be issued. + order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. + format: 'ldp_vc' | 'jwt_vc_json-ld'; } export interface CredentialSupportedJwtVcJson extends CommonCredentialSupported { + types: string[]; // REQUIRED. JSON array designating the types a certain credential type supports + credentialSubject?: IssuerCredentialSubject; // OPTIONAL. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value MAY be a dictionary, which allows to represent the full (potentially deeply nested) structure of the verifiable credential to be issued. + order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. format: 'jwt_vc_json'; } -export type CredentialSupported = CommonCredentialSupported & (CredentialSupportedJwtVcJson | CredentialSupportedJwtVcJsonLdAndLdpVc); +export interface SdJwtVcCredentialDefinition { + vct: string; // REQUIRED. JSON string designating the type of an SD-JWT vc + claims?: IssuerCredentialSubject; +} + +export interface CredentialSupportedSdJwtVc extends CommonCredentialSupported { + format: 'vc+sd-jwt'; + + // REQUIRED. JSON object containing the detailed description of the credential type + credential_definition: SdJwtVcCredentialDefinition; + order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. +} + +export type CredentialSupported = CommonCredentialSupported & + (CredentialSupportedJwtVcJson | CredentialSupportedJwtVcJsonLdAndLdpVc | CredentialSupportedSdJwtVc); -export interface CredentialOfferFormat { +export interface CommonCredentialOfferFormat { format: OID4VCICredentialFormat | string; - types: string[]; } +export interface CredentialOfferFormatJwtVcJsonLdAndLdpVc extends CommonCredentialOfferFormat { + format: 'ldp_vc' | 'jwt_vc_json-ld'; + // REQUIRED. JSON object containing (and isolating) the detailed description of the credential type. This object MUST be processed using full JSON-LD processing. + credential_definition: JsonLdIssuerCredentialDefinition; +} + +export interface CredentialOfferFormatJwtVcJson extends CommonCredentialOfferFormat { + format: 'jwt_vc_json'; + types: string[]; // REQUIRED. JSON array as defined in Appendix E.1.1.2. This claim contains the type values the Wallet shall request in the subsequent Credential Request. +} + +export interface CredentialOfferFormatSdJwtVc extends CommonCredentialOfferFormat { + format: 'vc+sd-jwt'; + credential_definition: SdJwtVcCredentialDefinition; +} + +export type CredentialOfferFormat = CommonCredentialOfferFormat & + (CredentialOfferFormatJwtVcJsonLdAndLdpVc | CredentialOfferFormatJwtVcJson | CredentialOfferFormatSdJwtVc); + /** * Optional storage that can help the credential Data Supplier. For instance to store credential input data during offer creation, if no additional data can be supplied later on */ @@ -107,10 +142,10 @@ export type CreateCredentialOfferURIResult = { userPinRequired: boolean; }; -export interface IssuerCredentialDefinition { +export interface JsonLdIssuerCredentialDefinition { '@context': ICredentialContextType[]; types: string[]; - credentialSubject: IssuerCredentialSubject; + credentialSubject?: IssuerCredentialSubject; } export interface ErrorResponse extends Response { @@ -127,15 +162,20 @@ export interface CommonCredentialRequest { proof?: ProofOfPossession; } -export interface CredentialRequestJwtVc extends CommonCredentialRequest { - format: 'jwt_vc_json' | 'jwt_vc_json-ld'; +export interface CredentialRequestJwtVcJson extends CommonCredentialRequest { + format: 'jwt_vc_json'; types: string[]; credentialSubject?: IssuerCredentialSubject; } -export interface CredentialRequestLdpVc extends CommonCredentialRequest { - format: 'ldp_vc'; - credential_definition: IssuerCredentialDefinition; +export interface CredentialRequestJwtVcJsonLdAndLdpVc extends CommonCredentialRequest { + format: 'ldp_vc' | 'jwt_vc_json-ld'; + credential_definition: JsonLdIssuerCredentialDefinition; +} + +export interface CredentialRequestSdJwtVc extends CommonCredentialRequest { + format: 'vc+sd-jwt'; + credential_definition: SdJwtVcCredentialDefinition; } export interface CommonCredentialResponse { @@ -156,6 +196,11 @@ export interface CredentialResponseJwtVc { credential: string; } +export interface CredentialResponseSdJwtVc { + format: 'vc+sd-jwt'; + credential: string; +} + // export type CredentialSubjectDisplay = NameAndLocale[]; export type IssuerCredentialSubjectDisplay = CredentialSubjectDisplay & { [key: string]: CredentialSubjectDisplay }; diff --git a/packages/common/lib/types/v1_0_08.types.ts b/packages/common/lib/types/v1_0_08.types.ts index fcdfc29e..53580b96 100644 --- a/packages/common/lib/types/v1_0_08.types.ts +++ b/packages/common/lib/types/v1_0_08.types.ts @@ -6,7 +6,7 @@ import { CredentialsSupportedDisplay, CredentialSupportedBrief, IssuerCredential export interface CredentialRequestV1_0_08 { type: string; format: CredentialFormat; - proof: ProofOfPossession; + proof?: ProofOfPossession; } export interface IssuerMetadataV1_0_08 { @@ -34,11 +34,16 @@ export interface CredentialSupportedTypeV1_0_08 { [credentialType: string]: CredentialSupportedV1_0_08; } +export interface CredentialSupportedFormatV1_0_08 extends CredentialSupportedBrief { + name?: string; + types: string[]; +} + export interface CredentialSupportedV1_0_08 { display?: CredentialsSupportedDisplay[]; formats: { // REQUIRED. A JSON object containing a list of key value pairs, where the key is a string identifying the format of the Credential. Below is a non-exhaustive list of valid key values defined by this specification: - [credentialFormat: string]: CredentialSupportedBrief; + [credentialFormat: string]: CredentialSupportedFormatV1_0_08; }; claims?: IssuerCredentialSubject; // REQUIRED. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value is a JSON object detailing the specifics about the support for the claim with a following non-exhaustive list of parameters that MAY be included: } diff --git a/packages/common/lib/types/v1_0_11.types.ts b/packages/common/lib/types/v1_0_11.types.ts index 51fbb7f2..302aeec8 100644 --- a/packages/common/lib/types/v1_0_11.types.ts +++ b/packages/common/lib/types/v1_0_11.types.ts @@ -3,10 +3,10 @@ import { CommonCredentialRequest, CredentialDataSupplierInput, CredentialOfferFormat, - CredentialRequestJwtVc, - CredentialRequestLdpVc, + CredentialRequestJwtVcJson, + CredentialRequestJwtVcJsonLdAndLdpVc, + CredentialRequestSdJwtVc, Grant, - IssuerCredentialDefinition, } from './Generic.types'; export interface CredentialOfferV1_0_11 { @@ -21,7 +21,7 @@ export interface CredentialOfferRESTRequest extends CredentialOfferV1_0_11 { credentialDataSupplierInput?: CredentialDataSupplierInput; } -export interface CommonCredentialOfferPayloadV1_0_11 { +export interface CredentialOfferPayloadV1_0_11 { /** * REQUIRED. The URL of the Credential Issuer, the Wallet is requested to obtain one or more Credentials from. */ @@ -48,22 +48,8 @@ export interface CommonCredentialOfferPayloadV1_0_11 { grants?: Grant; } -export interface CredentialOfferLdpVcV1_0_11 extends CommonCredentialOfferPayloadV1_0_11 { - /** - * REQUIRED. JSON object containing (and isolating) the detailed description of the credential type. - * This object MUST be processed using full JSON-LD processing. It consists of the following sub-claims: - * - @context: REQUIRED. JSON array as defined in Appendix E.1.3.2 - * - types: REQUIRED. JSON array as defined in Appendix E.1.3.2. - * This claim contains the type values the Wallet shall request in the subsequent Credential Request - */ - credential_definition: IssuerCredentialDefinition; -} - -export type CredentialOfferJwtVcV1_0_11 = CommonCredentialOfferPayloadV1_0_11; - -export type CredentialOfferPayloadV1_0_11 = CommonCredentialOfferPayloadV1_0_11 & (CredentialOfferLdpVcV1_0_11 | CredentialOfferJwtVcV1_0_11); - -export type CredentialRequestV1_0_11 = CommonCredentialRequest & (CredentialRequestJwtVc | CredentialRequestLdpVc); +export type CredentialRequestV1_0_11 = CommonCredentialRequest & + (CredentialRequestJwtVcJson | CredentialRequestJwtVcJsonLdAndLdpVc | CredentialRequestSdJwtVc); export interface AuthorizationRequestV1_0_11 extends AuthorizationDetailsJwtVcJson, AuthorizationDetailsJwtVcJson { issuer_state?: string; diff --git a/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts b/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts index 76d9d6cc..e47b7e97 100644 --- a/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts +++ b/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts @@ -4,7 +4,6 @@ import { Alg, CNonceState, CredentialIssuerMetadataOpts, - CredentialOfferLdpVcV1_0_11, CredentialOfferSession, IssueStatus, Jwt, @@ -45,18 +44,24 @@ describe('OID4VCIServer', () => { credentialOffer: { credential_offer: { credential_issuer: 'test_issuer', - credential_definition: { - '@context': ['test_context'], - types: ['VerifiableCredential'], - credentialSubject: {}, - }, + credentials: [ + { + format: 'ldp_vc', + credential_definition: { + '@context': ['test_context'], + types: ['VerifiableCredential'], + credentialSubject: {}, + }, + }, + ], + grants: { 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { user_pin_required: true, 'pre-authorized_code': preAuthorizedCode1, }, }, - } as CredentialOfferLdpVcV1_0_11, + }, }, } const credentialOfferState2: CredentialOfferSession = { @@ -75,7 +80,7 @@ describe('OID4VCIServer', () => { user_pin_required: false, }, }, - } as CredentialOfferLdpVcV1_0_11, + }, }, } const credentialOfferState3: CredentialOfferSession = { ...credentialOfferState1, preAuthorizedCode: preAuthorizedCode3, createdAt: 0 } @@ -165,7 +170,7 @@ describe('OID4VCIServer', () => { expect(res.statusCode).toEqual(200) const actual = JSON.parse(res.text) expect(actual).toEqual({ - access_token: expect.stringContaining('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE2O'), + access_token: expect.stringContaining('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOj'), token_type: 'bearer', expires_in: 300000, c_nonce: expect.any(String), diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 054ceb82..b8411f07 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -19,9 +19,9 @@ import { Grant, IAT_ERROR, ISSUER_CONFIG_ERROR, - IssuerCredentialDefinition, IssueStatus, IStateManager, + JsonLdIssuerCredentialDefinition, JWT_VERIFY_CONFIG_ERROR, JWTVerifyCallback, JwtVerifyResult, @@ -95,7 +95,7 @@ export class VcIssuer { public async createCredentialOfferURI(opts: { grants?: Grant credentials?: (CredentialOfferFormat | string)[] - credentialDefinition?: IssuerCredentialDefinition + credentialDefinition?: JsonLdIssuerCredentialDefinition credentialOfferUri?: string credentialDataSupplierInput?: CredentialDataSupplierInput // Optional storage that can help the credential Data Supplier. For instance to store credential input data during offer creation, if no additional data can be supplied later on baseUri?: string diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index dd5d9e97..e774bffd 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -1,7 +1,6 @@ import { OpenID4VCIClient } from '@sphereon/oid4vci-client' import { Alg, - CredentialOfferLdpVcV1_0_11, CredentialOfferSession, CredentialSupported, IssuerCredentialSubjectDisplay, @@ -58,11 +57,16 @@ describe('VcIssuer', () => { credentialOffer: { credential_offer: { credential_issuer: 'did:key:test', - credential_definition: { - types: ['VerifiableCredential'], - '@context': ['https://www.w3.org/2018/credentials/v1'], - credentialSubject: {}, - }, + credentials: [ + { + format: 'ldp_vc', + credential_definition: { + types: ['VerifiableCredential'], + '@context': ['https://www.w3.org/2018/credentials/v1'], + credentialSubject: {}, + }, + }, + ], grants: { authorization_code: { issuer_state: issuerState }, 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { @@ -70,7 +74,7 @@ describe('VcIssuer', () => { user_pin_required: true, }, }, - } as CredentialOfferLdpVcV1_0_11, + }, }, }) vcIssuer = new VcIssuerBuilder() diff --git a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts index 1edbd53c..1ba45598 100644 --- a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts +++ b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts @@ -1,6 +1,8 @@ import { CredentialsSupportedDisplay, CredentialSupported, + isFormat, + isNotFormat, IssuerCredentialSubject, IssuerCredentialSubjectDisplay, OID4VCICredentialFormat, @@ -38,6 +40,9 @@ export class CredentialSupportedBuilderV1_11 { } withTypes(type: string | string[]): CredentialSupportedBuilderV1_11 { + if (this.format === 'vc+sd-jwt' && Array.isArray(type) && type.length > 1) { + throw new Error('Only one type is allowed for vc+sd-jwt') + } this.types = Array.isArray(type) ? type : [type] return this } @@ -113,13 +118,26 @@ export class CredentialSupportedBuilderV1_11 { } if (!this.types) { throw new Error('types are required') - } else { - credentialSupported.types = this.types } - if (this.credentialSubject) { - credentialSupported.credentialSubject = this.credentialSubject + // SdJwtVc has a different format + if (isFormat(credentialSupported, 'vc+sd-jwt')) { + if (this.types.length > 1) { + throw new Error('Only one type is allowed for vc+sd-jwt') + } + credentialSupported.credential_definition = { + vct: this.types[0], + } } + // And else would work here, but this way we get the correct typing + else if (isNotFormat(credentialSupported, 'vc+sd-jwt')) { + credentialSupported.types = this.types + + if (this.credentialSubject) { + credentialSupported.credentialSubject = this.credentialSubject + } + } + if (this.cryptographicSuitesSupported) { credentialSupported.cryptographic_suites_supported = this.cryptographicSuitesSupported } diff --git a/packages/tsconfig.json b/packages/tsconfig.json index ed4dc9a4..5fa5bce5 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "tsconfig-base.json", + "extends": "./tsconfig-base.json", "files": [], "references": [ {