From 8fdf41d715cf867e75e9f91f36b1fd2e383ee9ef Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 8 Oct 2024 16:14:52 +0200 Subject: [PATCH 1/2] feat: PEX and OID4VP multi-vp support (#2039) Signed-off-by: Timo Glastra --- packages/anoncreds/package.json | 2 +- packages/core/package.json | 4 +- .../DifPresentationExchangeService.ts | 12 +- .../openid4vc-holder/__tests__/fixtures.ts | 4 +- .../__tests__/openid4vci-holder.test.ts | 4 +- .../OpenId4VcSiopVerifierService.ts | 17 +- .../__tests__/openid4vc-verifier.test.ts | 1 + .../router/authorizationEndpoint.ts | 14 +- packages/openid4vc/src/shared/utils.ts | 14 + .../openid4vc/tests/openid4vc.e2e.test.ts | 421 +++++++++++++++++- pnpm-lock.yaml | 265 +---------- 11 files changed, 482 insertions(+), 276 deletions(-) diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 2d43d708fa..8ff8155a01 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -28,7 +28,7 @@ "dependencies": { "@astronautlabs/jsonpath": "^1.1.2", "@credo-ts/core": "workspace:*", - "@sphereon/pex-models": "^2.2.4", + "@sphereon/pex-models": "^2.3.1", "big-integer": "^1.6.51", "bn.js": "^5.2.1", "class-transformer": "0.5.1", diff --git a/packages/core/package.json b/packages/core/package.json index aae49a085a..64a08420e2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,8 +40,8 @@ "@sd-jwt/sd-jwt-vc": "^0.7.0", "@sd-jwt/types": "^0.7.0", "@sd-jwt/utils": "^0.7.0", - "@sphereon/pex": "^5.0.0-unstable.8", - "@sphereon/pex-models": "^2.2.4", + "@sphereon/pex": "5.0.0-unstable.2", + "@sphereon/pex-models": "^2.3.1", "@sphereon/ssi-types": "0.29.1-unstable.121", "@stablelib/ed25519": "^1.0.2", "@types/ws": "^8.5.4", diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index c6ac3d30b7..c4ae048a4d 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -23,6 +23,7 @@ import type { } from '@sphereon/ssi-types' import { PEVersion, PEX, Status } from '@sphereon/pex' +import { PartialSdJwtDecodedVerifiableCredential } from '@sphereon/pex/dist/main/lib' import { injectable } from 'tsyringe' import { Hasher, getJwkFromKey } from '../../crypto' @@ -242,9 +243,10 @@ export class DifPresentationExchangeService { }) return { - verifiablePresentations: verifiablePresentationResultsWithFormat.flatMap((resultWithFormat) => - resultWithFormat.verifiablePresentationResult.verifiablePresentations.map((encoded) => - getVerifiablePresentationFromEncoded(agentContext, encoded) + verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) => + getVerifiablePresentationFromEncoded( + agentContext, + resultWithFormat.verifiablePresentationResult.verifiablePresentation ) ), presentationSubmission, @@ -502,7 +504,9 @@ export class DifPresentationExchangeService { return signedPresentation.encoded as W3CVerifiablePresentation } else if (presentationToCreate.claimFormat === ClaimFormat.SdJwtVc) { - const sdJwtInput = presentationInput as SdJwtDecodedVerifiableCredential + const sdJwtInput = presentationInput as + | SdJwtDecodedVerifiableCredential + | PartialSdJwtDecodedVerifiableCredential if (!domain) { throw new CredoError("Missing 'domain' property, unable to set required 'aud' property in SD-JWT KB-JWT") diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts index 183dd1b778..6f5b58d71c 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts @@ -126,8 +126,10 @@ export const matrrLaunchpadDraft11JwtVcJson = { } export const waltIdDraft11JwtVcJson = { - credentialOffer: + credentialOfferPreAuth: 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D', + credentialOfferAuth: + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22efc2f5dd-0f44-4f38-a902-3204e732c391%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D', getMetadataResponse: { issuer: 'https://issuer.portal.walt.id', authorization_endpoint: 'https://issuer.portal.walt.id/authorize', diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts index fc4efc421d..2d37bfd355 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts @@ -136,7 +136,7 @@ describe('OpenId4VcHolder', () => { .post('/credential') .reply(200, fixture.credentialResponse) - const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOfferPreAuth) await expect(() => holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, { @@ -286,7 +286,7 @@ describe('OpenId4VcHolder', () => { .reply(404) const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer + fixture.credentialOfferAuth ) const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveIssuanceAuthorizationRequest( diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index c6b511b2be..9d4b179c7e 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -242,8 +242,8 @@ export class OpenId4VcSiopVerifierService { const relyingParty = await this.getRelyingParty(agentContext, options.verificationSession.verifierId, { presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition, - clientId: requestClientId, authorizationResponseUrl, + clientId: requestClientId, }) // This is very unfortunate, but storing state in sphereon's SiOP-OID4VP library @@ -331,10 +331,12 @@ export class OpenId4VcSiopVerifierService { throw new CredoError('Unable to extract submission from the response.') } - const vps = Array.isArray(presentations) ? presentations : [presentations] + // FIXME: should return type be an array? As now it doesn't always match the submission + const presentationsArray = Array.isArray(presentations) ? presentations : [presentations] + presentationExchange = { definition: presentationDefinitions[0].definition, - presentations: vps.map(getVerifiablePresentationFromSphereonWrapped), + presentations: presentationsArray.map(getVerifiablePresentationFromSphereonWrapped), submission, } } @@ -459,10 +461,10 @@ export class OpenId4VcSiopVerifierService { responseMode, }: { responseMode?: ResponseMode - authorizationResponseUrl: string idToken?: boolean presentationDefinition?: DifPresentationExchangeDefinition clientId: string + authorizationResponseUrl: string clientIdScheme?: ClientIdScheme } ) { @@ -519,6 +521,7 @@ export class OpenId4VcSiopVerifierService { : undefined builder + .withClientId(clientId) .withResponseUri(authorizationResponseUrl) .withIssuer(ResponseIss.SELF_ISSUED_V2) .withAudience(RequestAud.SELF_ISSUED_V2) @@ -541,9 +544,11 @@ export class OpenId4VcSiopVerifierService { // TODO: we should probably allow some dynamic values here .withClientMetadata({ - client_id: clientId, ...jarmClientMetadata, - client_id_scheme: clientIdScheme, + // FIXME: not passing client_id here means it will not be added + // to the authorization request url (not the signed payload). Need + // to fix that in Sphereon lib + client_id: clientId, passBy: PassBy.VALUE, responseTypesSupported: [ResponseType.VP_TOKEN], subject_syntax_types_supported: supportedDidMethods.map((m) => `did:${m}`), diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts index d1d1662649..e40ef70579 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts @@ -62,6 +62,7 @@ describe('OpenId4VcVerifier', () => { expect(jwt.header.kid).toEqual(verifier.kid) expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) expect(jwt.header.typ).toEqual('JWT') + expect(jwt.payload.additionalClaims.scope).toEqual(undefined) expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did) expect(jwt.payload.additionalClaims.response_uri).toEqual( `http://redirect-uri/${openIdVerifier.verifierId}/authorize` diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts index ee26523d96..99c95ff974 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -103,16 +103,22 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc nonce: authorizationResponsePayload.nonce, }) } - if (typeof authorizationResponsePayload.presentation_submission === 'string') { - authorizationResponsePayload.presentation_submission = JSON.parse( - authorizationResponsePayload.presentation_submission - ) + authorizationResponsePayload.presentation_submission = JSON.parse(request.body.presentation_submission) + } + + // This feels hacky, and should probably be moved to OID4VP lib. However the OID4VP spec allows either object, string, or array... + if ( + typeof authorizationResponsePayload.vp_token === 'string' && + (authorizationResponsePayload.vp_token.startsWith('{') || authorizationResponsePayload.vp_token.startsWith('[')) + ) { + authorizationResponsePayload.vp_token = JSON.parse(authorizationResponsePayload.vp_token) } if (!verificationSession) { throw new CredoError('Missing verification session, cannot verify authorization response.') } + await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, { authorizationResponse: authorizationResponsePayload, verificationSession, diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index c5e46aa027..f47fca1d1f 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -11,6 +11,7 @@ import { JwtPayload, SignatureSuiteRegistry, X509Service, + getDomainFromUrl, getJwkClassFromKeyType, getJwkFromJson, getJwkFromKey, @@ -135,6 +136,19 @@ export async function openIdTokenIssuerToJwtIssuer( throw new CredoError(`No supported signature algorithms found key type: '${jwk.keyType}'`) } + if (!openId4VcTokenIssuer.issuer.startsWith('https://')) { + throw new CredoError('The X509 certificate issuer must be a HTTPS URI.') + } + + if ( + !leafCertificate.sanUriNames?.includes(openId4VcTokenIssuer.issuer) && + !leafCertificate.sanDnsNames?.includes(getDomainFromUrl(openId4VcTokenIssuer.issuer)) + ) { + throw new Error( + `The 'iss' claim in the payload does not match a 'SAN-URI' or 'SAN-DNS' name in the x5c certificate.` + ) + } + return { ...openId4VcTokenIssuer, alg, diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index ec6101437e..1409395523 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -30,6 +30,7 @@ import { OpenId4VcHolderModule, OpenId4VcIssuanceSessionState, OpenId4VcIssuerModule, + OpenId4VcVerificationSessionRepository, OpenId4VcVerificationSessionState, OpenId4VcVerifierModule, } from '../src' @@ -768,6 +769,12 @@ describe('OpenId4Vc', () => { ], } satisfies DifPresentationExchangeDefinitionV2 + // Hack to make it work with x5c check + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl = + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl.replace('http://', 'https://') + const { authorizationRequest, verificationSession } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ verifierId: openIdVerifier.verifierId, @@ -782,14 +789,28 @@ describe('OpenId4Vc', () => { }, }) - expect(authorizationRequest).toEqual( + // Hack to make it work with x5c checks + verificationSession.authorizationRequestUri = verificationSession.authorizationRequestUri.replace('https', 'http') + const verificationSessionRepoitory = verifier.agent.dependencyManager.resolve( + OpenId4VcVerificationSessionRepository + ) + await verificationSessionRepoitory.update(verifier.agent.context, verificationSession) + + // Hack to make it work with x5c check + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl = + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl.replace('https://', 'http://') + + expect(authorizationRequest.replace('https', 'http')).toEqual( `openid4vp://?client_id=localhost%3A1234&request_uri=${encodeURIComponent( verificationSession.authorizationRequestUri )}` ) const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( - authorizationRequest + // hack to make it work on localhost + authorizationRequest.replace('https', 'http') ) expect(resolvedAuthorizationRequest.authorizationRequest.payload?.response_mode).toEqual('direct_post.jwt') @@ -842,6 +863,10 @@ describe('OpenId4Vc', () => { resolvedAuthorizationRequest.presentationExchange.credentialsForRequest ) + // Hack to make it work with x5c + resolvedAuthorizationRequest.authorizationRequest.responseURI = + resolvedAuthorizationRequest.authorizationRequest.responseURI?.replace('https', 'http') + const { serverResponse, submittedResponse } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, @@ -1001,6 +1026,12 @@ describe('OpenId4Vc', () => { ], } satisfies DifPresentationExchangeDefinitionV2 + // Hack to make it work with x5c check + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl = + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl.replace('http://', 'https://') + const { authorizationRequest, verificationSession } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ verifierId: openIdVerifier.verifierId, @@ -1014,14 +1045,28 @@ describe('OpenId4Vc', () => { }, }) - expect(authorizationRequest).toEqual( + // Hack to make it work with x5c checks + verificationSession.authorizationRequestUri = verificationSession.authorizationRequestUri.replace('https', 'http') + const verificationSessionRepoitory = verifier.agent.dependencyManager.resolve( + OpenId4VcVerificationSessionRepository + ) + await verificationSessionRepoitory.update(verifier.agent.context, verificationSession) + + // Hack to make it work with x5c check + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl = + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl.replace('https://', 'http://') + + expect(authorizationRequest.replace('https', 'http')).toEqual( `openid4vp://?client_id=localhost%3A1234&request_uri=${encodeURIComponent( verificationSession.authorizationRequestUri )}` ) const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( - authorizationRequest + // hack to make it work on localhost + authorizationRequest.replace('https', 'http') ) expect(resolvedAuthorizationRequest.presentationExchange?.credentialsForRequest).toEqual({ @@ -1073,6 +1118,10 @@ describe('OpenId4Vc', () => { resolvedAuthorizationRequest.presentationExchange.credentialsForRequest ) + // Hack to make it work with x5c + resolvedAuthorizationRequest.authorizationRequest.responseURI = + resolvedAuthorizationRequest.authorizationRequest.responseURI?.replace('https', 'http') + const { serverResponse, submittedResponse } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, @@ -1171,4 +1220,368 @@ describe('OpenId4Vc', () => { ], }) }) + + it('e2e flow with verifier endpoints verifying two sd-jwt-vcs with selective disclosure', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + + const signedSdJwtVc = await issuer.agent.sdJwtVc.sign({ + holder: { method: 'did', didUrl: holder.kid }, + issuer: { + method: 'did', + didUrl: issuer.kid, + }, + payload: { + vct: 'OpenBadgeCredential', + university: 'innsbruck', + degree: 'bachelor', + name: 'John Doe', + }, + disclosureFrame: { + _sd: ['university', 'name'], + }, + }) + + const signedSdJwtVc2 = await issuer.agent.sdJwtVc.sign({ + holder: { method: 'did', didUrl: holder.kid }, + issuer: { + method: 'did', + didUrl: issuer.kid, + }, + payload: { + vct: 'OpenBadgeCredential2', + university: 'innsbruck2', + degree: 'bachelor2', + name: 'John Doe2', + }, + disclosureFrame: { + _sd: ['university', 'name'], + }, + }) + + const certificate = await verifier.agent.x509.createSelfSignedCertificate({ + key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: [[{ type: 'dns', value: 'localhost:1234' }]], + }) + + const rawCertificate = certificate.toString('base64') + await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) + await holder.agent.sdJwtVc.store(signedSdJwtVc2.compact) + + await holder.agent.x509.addTrustedCertificate(rawCertificate) + await verifier.agent.x509.addTrustedCertificate(rawCertificate) + + const presentationDefinition = { + id: 'OpenBadgeCredentials', + input_descriptors: [ + { + id: 'OpenBadgeCredentialDescriptor', + format: { + 'vc+sd-jwt': { + 'sd-jwt_alg_values': ['EdDSA'], + }, + }, + constraints: { + limit_disclosure: 'required', + fields: [ + { + path: ['$.vct'], + filter: { + type: 'string', + const: 'OpenBadgeCredential', + }, + }, + { + path: ['$.university'], + }, + ], + }, + }, + { + id: 'OpenBadgeCredentialDescriptor2', + format: { + 'vc+sd-jwt': { + 'sd-jwt_alg_values': ['EdDSA'], + }, + }, + constraints: { + limit_disclosure: 'required', + fields: [ + { + path: ['$.vct'], + filter: { + type: 'string', + const: 'OpenBadgeCredential2', + }, + }, + { + path: ['$.name'], + }, + ], + }, + }, + ], + } satisfies DifPresentationExchangeDefinitionV2 + + // Hack to make it work with x5c check + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl = + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl.replace('http://', 'https://') + const { authorizationRequest, verificationSession } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifier.verifierId, + + requestSigner: { + method: 'x5c', + x5c: [rawCertificate], + }, + presentationExchange: { + definition: presentationDefinition, + }, + }) + + // Hack to make it work with x5c checks + verificationSession.authorizationRequestUri = verificationSession.authorizationRequestUri.replace('https', 'http') + const verificationSessionRepoitory = verifier.agent.dependencyManager.resolve( + OpenId4VcVerificationSessionRepository + ) + await verificationSessionRepoitory.update(verifier.agent.context, verificationSession) + + // Hack to make it work with x5c check + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl = + // @ts-expect-error + verifier.agent.modules.openId4VcVerifier.config.options.baseUrl.replace('https://', 'http://') + + expect(authorizationRequest.replace('https', 'http')).toEqual( + `openid4vp://?client_id=localhost%3A1234&request_uri=${encodeURIComponent( + verificationSession.authorizationRequestUri + )}` + ) + + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + // hack to make it work on localhost + authorizationRequest.replace('https', 'http') + ) + + expect(resolvedAuthorizationRequest.presentationExchange?.credentialsForRequest).toEqual({ + areRequirementsSatisfied: true, + name: undefined, + purpose: undefined, + requirements: expect.arrayContaining([ + { + isRequirementSatisfied: true, + needsCount: 1, + rule: 'pick', + submissionEntry: [ + { + name: undefined, + purpose: undefined, + inputDescriptorId: 'OpenBadgeCredentialDescriptor', + verifiableCredentials: [ + { + type: ClaimFormat.SdJwtVc, + credentialRecord: expect.objectContaining({ + compactSdJwtVc: signedSdJwtVc.compact, + }), + // Name is NOT in here + disclosedPayload: { + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + degree: 'bachelor', + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + university: 'innsbruck', + vct: 'OpenBadgeCredential', + }, + }, + ], + }, + ], + }, + { + isRequirementSatisfied: true, + needsCount: 1, + rule: 'pick', + submissionEntry: [ + { + name: undefined, + purpose: undefined, + inputDescriptorId: 'OpenBadgeCredentialDescriptor2', + verifiableCredentials: [ + { + type: ClaimFormat.SdJwtVc, + credentialRecord: expect.objectContaining({ + compactSdJwtVc: signedSdJwtVc2.compact, + }), + disclosedPayload: { + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + vct: 'OpenBadgeCredential2', + degree: 'bachelor2', + name: 'John Doe2', + }, + }, + ], + }, + ], + }, + ]), + }) + + if (!resolvedAuthorizationRequest.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + // TODO: better way to auto-select + const presentationExchangeService = holder.agent.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedAuthorizationRequest.presentationExchange.credentialsForRequest + ) + + // Hack to make it work with x5c + resolvedAuthorizationRequest.authorizationRequest.responseURI = + resolvedAuthorizationRequest.authorizationRequest.responseURI?.replace('https', 'http') + + const { serverResponse, submittedResponse } = + await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + // path_nested should not be used for sd-jwt + expect(submittedResponse.presentation_submission?.descriptor_map[0].path_nested).toBeUndefined() + expect(submittedResponse).toEqual({ + presentation_submission: { + definition_id: 'OpenBadgeCredentials', + descriptor_map: [ + { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredentialDescriptor', + path: '$[0]', + }, + { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredentialDescriptor2', + path: '$[1]', + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: [expect.any(String), expect.any(String)], + }) + expect(serverResponse).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifier.agent.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession.id, + }) + const { idToken, presentationExchange } = + await verifier.agent.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession.id) + + expect(idToken).toBeUndefined() + + const presentation = presentationExchange?.presentations[0] as SdJwtVc + + // name SHOULD NOT be disclosed + expect(presentation.prettyClaims).not.toHaveProperty('name') + + // university and name SHOULD NOT be in the signed payload + expect(presentation.payload).not.toHaveProperty('university') + expect(presentation.payload).not.toHaveProperty('name') + + expect(presentationExchange).toEqual({ + definition: presentationDefinition, + submission: { + definition_id: 'OpenBadgeCredentials', + descriptor_map: [ + { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredentialDescriptor', + path: '$[0]', + }, + { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredentialDescriptor2', + path: '$[1]', + }, + ], + id: expect.any(String), + }, + presentations: [ + { + compact: expect.any(String), + header: { + alg: 'EdDSA', + kid: '#z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + typ: 'vc+sd-jwt', + }, + payload: { + _sd: [expect.any(String), expect.any(String)], + _sd_alg: 'sha-256', + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + vct: 'OpenBadgeCredential', + degree: 'bachelor', + }, + // university SHOULD be disclosed + prettyClaims: { + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + vct: 'OpenBadgeCredential', + degree: 'bachelor', + university: 'innsbruck', + }, + }, + { + compact: expect.any(String), + header: { + alg: 'EdDSA', + kid: '#z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + typ: 'vc+sd-jwt', + }, + payload: { + _sd: [expect.any(String), expect.any(String)], + _sd_alg: 'sha-256', + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + vct: 'OpenBadgeCredential2', + degree: 'bachelor2', + }, + prettyClaims: { + cnf: { + kid: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + iat: expect.any(Number), + iss: 'did:key:z6MkrzQPBr4pyqC776KKtrz13SchM5ePPbssuPuQZb5t4uKQ', + vct: 'OpenBadgeCredential2', + name: 'John Doe2', + degree: 'bachelor2', + }, + }, + ], + }) + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8eae3b63d..f083c623db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,8 +237,8 @@ importers: specifier: workspace:* version: link:../core '@sphereon/pex-models': - specifier: ^2.2.4 - version: 2.2.4 + specifier: ^2.3.1 + version: 2.3.1 big-integer: specifier: ^1.6.51 version: 1.6.52 @@ -448,11 +448,11 @@ importers: specifier: ^0.7.0 version: 0.7.2 '@sphereon/pex': - specifier: ^5.0.0-unstable.8 - version: 5.0.0-unstable.17(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) + specifier: 5.0.0-unstable.2 + version: 5.0.0-unstable.2(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) '@sphereon/pex-models': - specifier: ^2.2.4 - version: 2.2.4 + specifier: ^2.3.1 + version: 2.3.1 '@sphereon/ssi-types': specifier: 0.29.1-unstable.121 version: 0.29.1-unstable.121(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) @@ -2372,8 +2372,8 @@ packages: resolution: {integrity: sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==} engines: {node: '>=14.15'} - '@scure/base@1.1.9': - resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + '@scure/base@1.1.8': + resolution: {integrity: sha512-6CyAclxj3Nb0XT7GHK6K4zK6k2xJm6E4Ft0Ohjt4WgegiFUHEtFb2CGzmPmGBwoIhrLsqNLYfLr04Y1GePrzZg==} '@sd-jwt/core@0.7.2': resolution: {integrity: sha512-vix1GplUFc1A9H42r/yXkg7cKYthggyqZEwlFdsBbn4xdZNE+AHVF4N7kPa1pPxipwN3UIHd4XnQ5MJV15mhsQ==} @@ -2479,16 +2479,9 @@ packages: awesome-qr: optional: true - '@sphereon/pex-models@2.2.4': - resolution: {integrity: sha512-pGlp+wplneE1+Lk3U48/2htYKTbONMeG5/x7vhO6AnPUOsnOXeJdftPrBYWVSzz/JH5GJptAc6+pAyYE1zMu4Q==} - '@sphereon/pex-models@2.3.1': resolution: {integrity: sha512-SByU4cJ0XYA6VZQ/L6lsSiRcFtBPHbFioCeQ4GP7/W/jQ+PSBD7uK2oTnKQ9/0iEiMK/6JYqhKgLs4a9UX3UTQ==} - '@sphereon/pex@5.0.0-unstable.17': - resolution: {integrity: sha512-wriK0JGw/ONrISvYFbE//+ITVLZnvcPjPesFrMpZcDhxlOLf+TMOqVtUjjg2DHWy7HINM8WQ2eUKz1d96vyarw==} - engines: {node: '>=18'} - '@sphereon/pex@5.0.0-unstable.2': resolution: {integrity: sha512-mA6lY/OBKKzsh4Jf4btm9Tj4ymVsX6xuVATn85LurD4bt3fhZwNJMkxhFy4tT/QyAtp05E4aaEq0wTVvOjVa7w==} engines: {node: '>=18'} @@ -2496,39 +2489,21 @@ packages: '@sphereon/ssi-sdk-ext.did-utils@0.24.1-unstable.112': resolution: {integrity: sha512-nc0jFPOWg0H20S8m83aQUpNym0Wx0rJCGkgpH6GdK8gBtgza8Y9DvAap1AYZug18WbqPcF6rBjvtIJqAKsSvlQ==} - '@sphereon/ssi-sdk-ext.did-utils@0.24.1-unstable.130': - resolution: {integrity: sha512-I+0VjitRjisABWm8RtTPQG57tFwfUS13Wud30OvBoADRxnaA0guUrkS82AYtV6YD0TBHdrd0D6a0RCJwK9SvDg==} - '@sphereon/ssi-sdk-ext.identifier-resolution@0.24.1-unstable.112': resolution: {integrity: sha512-VBkJjHokFNsQ0wsHUbyCysMuShTOEuK6yrvyW64uOFcB2hzq1J/wi9CewI+YRHv7mnejBlu46uYNycvOKKRcsQ==} - '@sphereon/ssi-sdk-ext.identifier-resolution@0.24.1-unstable.130': - resolution: {integrity: sha512-9mY+qgXmbZCC8aic99R7B3vKBHBakDiC6Sktgd7Q9AknR8cCmvdrmTgnOETrLng9L43uNOJnNTMG/4T6LqmtsA==} - '@sphereon/ssi-sdk-ext.jwt-service@0.24.1-unstable.112': resolution: {integrity: sha512-OrBaSg5wLSehkJ4MyuyDWKD4CRIBERnJqRT0o/y5DbaCF3k02+/lN/rWP+4qwk2w192fIEAExG4L2GwZM/5PLQ==} - '@sphereon/ssi-sdk-ext.jwt-service@0.24.1-unstable.130': - resolution: {integrity: sha512-MHLGRmJODEYJyFoXKwlKMYzf48vS5JcUkGk0W4sqmrY1wwcw+ro3l8adIprG37mNuknXBs9Mv0x/tvibE9wwCQ==} - '@sphereon/ssi-sdk-ext.key-manager@0.24.1-unstable.112': resolution: {integrity: sha512-XdXV4qj+BYTZWyGHduWQxl0mxCYt5CF0Q93p4Thbm2/hjfaAC6aJi2WAXFGTIri95QVbKW1Uscob0CjNCVkWdg==} - '@sphereon/ssi-sdk-ext.key-manager@0.24.1-unstable.130': - resolution: {integrity: sha512-O/6NlKmlYRnEyP/mAI2Diu0qptMSqZfVwqog8KAOG/G8JUmktfSQmclBW8RoJ6AD9uY65BGzNk1oAVuuMv4Dog==} - '@sphereon/ssi-sdk-ext.key-utils@0.24.1-unstable.112': resolution: {integrity: sha512-er6TwGUWxlao2lSP97r1DTFlUXcPSMsIToULOWQJp6wKbvCuvV6pN5luS0qKB/W0/TOUE5kXzFwNx086BPnPRA==} - '@sphereon/ssi-sdk-ext.key-utils@0.24.1-unstable.130': - resolution: {integrity: sha512-DCyXW18g1OAuZ+aFHzQGrbZSx793DX94LSFnrWlOTMnYeILmrizuFksUlWSb3lTqQGAqWBC48NoR3I1H6lSMEQ==} - '@sphereon/ssi-sdk-ext.x509-utils@0.24.1-unstable.112': resolution: {integrity: sha512-bbx2jFoqWhW/xYABVwg3HiUo15yztPt3s+9bJtdB8n4PCjin4Nq3+vFvaHsmu70yAGkbWfsBcBVW6Y3oFtvpAA==} - '@sphereon/ssi-sdk-ext.x509-utils@0.24.1-unstable.130': - resolution: {integrity: sha512-JDX8i0WrwONaOivZXB+OxJQGkln7vuSLS61tOYl7M1RyPGixdBYuEuACsdvWf6egYOpaWmhmXZzaAOj18eDddw==} - '@sphereon/ssi-sdk.agent-config@0.29.1-unstable.161': resolution: {integrity: sha512-ZP/TjapF/Gv/AwnNr9e1U3rjyRwdLtAj4un9j1csnKcgYe9ff2fhYbe06y9mU4tfQilH69mAW4Tz1t6N5U7XbA==} @@ -2544,9 +2519,6 @@ packages: '@sphereon/ssi-types@0.29.1-unstable.208': resolution: {integrity: sha512-3YAFzy//BojsYN+RYoEjndWP3w5a8a3qRZi5dS0Gh6s4yMCiykqTJM1agJVeoaLce8JxFFaCWSpkzwbmJYGTaQ==} - '@sphereon/ssi-types@0.30.1': - resolution: {integrity: sha512-vbYaxQXb71sOPwDj7TRDlUGfIHKVVs8PiHfImPBgSBshrD7VpEHOrB+EwwavMm5MAQvWK/yblGmzk7FHds7SHA==} - '@sphereon/ssi-types@0.9.0': resolution: {integrity: sha512-umCr/syNcmvMMbQ+i/r/mwjI1Qw2aFPp9AwBTvTo1ailAVaaJjJGPkkVz1K9/2NZATNdDiQ3A8yGzdVJoKh9pA==} @@ -10136,7 +10108,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@scure/base@1.1.9': {} + '@scure/base@1.1.8': {} '@sd-jwt/core@0.7.2': dependencies: @@ -10324,44 +10296,8 @@ snapshots: - encoding - supports-color - '@sphereon/pex-models@2.2.4': {} - '@sphereon/pex-models@2.3.1': {} - '@sphereon/pex@5.0.0-unstable.17(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': - dependencies: - '@astronautlabs/jsonpath': 1.1.2 - '@sd-jwt/decode': 0.7.2 - '@sd-jwt/present': 0.7.2 - '@sd-jwt/types': 0.7.2 - '@sphereon/pex-models': 2.3.1 - '@sphereon/ssi-types': 0.30.1(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - jwt-decode: 3.1.2 - nanoid: 3.3.7 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - '@google-cloud/spanner' - - '@sap/hana-client' - - better-sqlite3 - - encoding - - hdb-pool - - ioredis - - mongodb - - mssql - - mysql2 - - oracledb - - pg - - pg-native - - pg-query-stream - - redis - - sql.js - - sqlite3 - - supports-color - - ts-node - - typeorm-aurora-data-api-driver - '@sphereon/pex@5.0.0-unstable.2(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': dependencies: '@astronautlabs/jsonpath': 1.1.2 @@ -10435,44 +10371,6 @@ snapshots: - ts-node - typeorm-aurora-data-api-driver - '@sphereon/ssi-sdk-ext.did-utils@0.24.1-unstable.130(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': - dependencies: - '@ethersproject/networks': 5.7.1 - '@ethersproject/transactions': 5.7.0 - '@sphereon/did-uni-client': 0.6.3 - '@sphereon/ssi-sdk-ext.key-utils': 0.24.1-unstable.130 - '@sphereon/ssi-sdk-ext.x509-utils': 0.24.1-unstable.130 - '@sphereon/ssi-sdk.agent-config': 0.29.1-unstable.161(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - '@sphereon/ssi-sdk.core': 0.29.1-unstable.161 - '@sphereon/ssi-types': 0.29.1-unstable.161 - '@stablelib/ed25519': 1.0.3 - '@veramo/core': 4.2.0 - '@veramo/utils': 4.2.0 - did-jwt: 6.11.6 - did-resolver: 4.1.0 - elliptic: 6.5.7 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - '@google-cloud/spanner' - - '@sap/hana-client' - - better-sqlite3 - - encoding - - hdb-pool - - ioredis - - mongodb - - mssql - - mysql2 - - oracledb - - pg - - pg-native - - pg-query-stream - - redis - - sql.js - - sqlite3 - - supports-color - - ts-node - - typeorm-aurora-data-api-driver - '@sphereon/ssi-sdk-ext.identifier-resolution@0.24.1-unstable.112(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': dependencies: '@sphereon/ssi-sdk-ext.did-utils': 0.24.1-unstable.112(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) @@ -10506,39 +10404,6 @@ snapshots: - ts-node - typeorm-aurora-data-api-driver - '@sphereon/ssi-sdk-ext.identifier-resolution@0.24.1-unstable.130(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': - dependencies: - '@sphereon/ssi-sdk-ext.did-utils': 0.24.1-unstable.130(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - '@sphereon/ssi-sdk-ext.key-utils': 0.24.1-unstable.130 - '@sphereon/ssi-sdk-ext.x509-utils': 0.24.1-unstable.130 - '@sphereon/ssi-sdk.agent-config': 0.29.1-unstable.161(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - '@sphereon/ssi-types': 0.29.1-unstable.161 - '@veramo/core': 4.2.0 - '@veramo/utils': 4.2.0 - debug: 4.3.6 - pkijs: 3.2.4 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - '@google-cloud/spanner' - - '@sap/hana-client' - - better-sqlite3 - - encoding - - hdb-pool - - ioredis - - mongodb - - mssql - - mysql2 - - oracledb - - pg - - pg-native - - pg-query-stream - - redis - - sql.js - - sqlite3 - - supports-color - - ts-node - - typeorm-aurora-data-api-driver - '@sphereon/ssi-sdk-ext.jwt-service@0.24.1-unstable.112(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': dependencies: '@sphereon/ssi-sdk-ext.did-utils': 0.24.1-unstable.112(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) @@ -10574,41 +10439,6 @@ snapshots: - ts-node - typeorm-aurora-data-api-driver - '@sphereon/ssi-sdk-ext.jwt-service@0.24.1-unstable.130(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': - dependencies: - '@sphereon/ssi-sdk-ext.did-utils': 0.24.1-unstable.130(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - '@sphereon/ssi-sdk-ext.identifier-resolution': 0.24.1-unstable.130(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - '@sphereon/ssi-sdk-ext.key-manager': 0.24.1-unstable.130 - '@sphereon/ssi-sdk-ext.key-utils': 0.24.1-unstable.130 - '@sphereon/ssi-sdk-ext.x509-utils': 0.24.1-unstable.130 - '@sphereon/ssi-sdk.agent-config': 0.29.1-unstable.161(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - '@sphereon/ssi-types': 0.29.1-unstable.161 - '@veramo/core': 4.2.0 - '@veramo/utils': 4.2.0 - debug: 4.3.6 - jwt-decode: 4.0.0 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - '@google-cloud/spanner' - - '@sap/hana-client' - - better-sqlite3 - - encoding - - hdb-pool - - ioredis - - mongodb - - mssql - - mysql2 - - oracledb - - pg - - pg-native - - pg-query-stream - - redis - - sql.js - - sqlite3 - - supports-color - - ts-node - - typeorm-aurora-data-api-driver - '@sphereon/ssi-sdk-ext.key-manager@0.24.1-unstable.112': dependencies: '@veramo/core': 4.2.0 @@ -10617,14 +10447,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sphereon/ssi-sdk-ext.key-manager@0.24.1-unstable.130': - dependencies: - '@veramo/core': 4.2.0 - '@veramo/key-manager': 4.2.0 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - supports-color - '@sphereon/ssi-sdk-ext.key-utils@0.24.1-unstable.112': dependencies: '@ethersproject/random': 5.7.0 @@ -10647,28 +10469,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sphereon/ssi-sdk-ext.key-utils@0.24.1-unstable.130': - dependencies: - '@ethersproject/random': 5.7.0 - '@sphereon/ssi-sdk-ext.x509-utils': 0.24.1-unstable.130 - '@sphereon/ssi-types': 0.29.1-unstable.161 - '@stablelib/ed25519': 1.0.3 - '@stablelib/sha256': 1.0.1 - '@stablelib/sha512': 1.0.1 - '@trust/keyto': 1.0.1 - '@veramo/core': 4.2.0 - base64url: 3.0.1 - debug: 4.3.6 - did-resolver: 4.1.0 - elliptic: 6.5.7 - lodash.isplainobject: 4.0.6 - multiformats: 9.9.0 - uint8arrays: 3.1.1 - varint: 6.0.0 - web-encoding: 1.1.5 - transitivePeerDependencies: - - supports-color - '@sphereon/ssi-sdk-ext.x509-utils@0.24.1-unstable.112': dependencies: '@trust/keyto': 1.0.1 @@ -10679,16 +10479,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sphereon/ssi-sdk-ext.x509-utils@0.24.1-unstable.130': - dependencies: - '@trust/keyto': 1.0.1 - debug: 4.3.6 - js-x509-utils: 1.0.7 - pkijs: 3.2.4 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - supports-color - '@sphereon/ssi-sdk.agent-config@0.29.1-unstable.161(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': dependencies: '@veramo/core': 4.2.0 @@ -10777,35 +10567,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sphereon/ssi-types@0.30.1(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4))': - dependencies: - '@sd-jwt/decode': 0.7.2 - '@sphereon/kmp-mdl-mdoc': 0.2.0-SNAPSHOT.22 - '@sphereon/ssi-sdk-ext.jwt-service': 0.24.1-unstable.130(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)) - debug: 4.3.6 - events: 3.3.0 - jwt-decode: 3.1.2 - transitivePeerDependencies: - - '@google-cloud/spanner' - - '@sap/hana-client' - - better-sqlite3 - - encoding - - hdb-pool - - ioredis - - mongodb - - mssql - - mysql2 - - oracledb - - pg - - pg-native - - pg-query-stream - - redis - - sql.js - - sqlite3 - - supports-color - - ts-node - - typeorm-aurora-data-api-driver - '@sphereon/ssi-types@0.9.0': dependencies: jwt-decode: 3.1.2 @@ -12299,8 +12060,8 @@ snapshots: dependencies: '@noble/ciphers': 0.4.1 '@noble/curves': 1.6.0 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.9 + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.8 canonicalize: 2.0.0 did-resolver: 4.1.0 multibase: 4.0.6 @@ -12312,7 +12073,7 @@ snapshots: '@noble/ciphers': 0.5.3 '@noble/curves': 1.6.0 '@noble/hashes': 1.4.0 - '@scure/base': 1.1.9 + '@scure/base': 1.1.8 canonicalize: 2.0.0 did-resolver: 4.1.0 multibase: 4.0.6 @@ -15177,7 +14938,7 @@ snapshots: pkijs@3.2.4: dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 asn1js: 3.0.5 bytestreamjs: 2.0.1 pvtsutils: 1.3.5 From 08a485b4912e9093ab48af3e816df9c1cc073443 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Tue, 8 Oct 2024 18:17:18 +0200 Subject: [PATCH 2/2] feat: trusted certificates hook for presentations (#2052) Signed-off-by: Tom Lanser --- packages/core/src/crypto/JwsService.ts | 20 ++++++++++----- ...fPresentationExchangeProofFormatService.ts | 5 +++- .../modules/vc/W3cCredentialServiceOptions.ts | 14 +++++++++++ .../vc/jwt-vc/W3cJwtCredentialService.ts | 8 ++++++ .../__tests__/W3cJwtCredentialService.test.ts | 2 ++ .../core/src/modules/x509/X509ModuleConfig.ts | 25 +++++++++++++++++++ .../OpenId4VcSiopVerifierService.ts | 6 ++++- 7 files changed, 72 insertions(+), 8 deletions(-) diff --git a/packages/core/src/crypto/JwsService.ts b/packages/core/src/crypto/JwsService.ts index 88b29d2ebf..eed23e2df2 100644 --- a/packages/core/src/crypto/JwsService.ts +++ b/packages/core/src/crypto/JwsService.ts @@ -110,7 +110,10 @@ export class JwsService { /** * Verify a JWS */ - public async verifyJws(agentContext: AgentContext, { jws, jwkResolver }: VerifyJwsOptions): Promise { + public async verifyJws( + agentContext: AgentContext, + { jws, jwkResolver, trustedCertificates }: VerifyJwsOptions + ): Promise { let signatures: JwsDetachedFormat[] = [] let payload: string @@ -162,6 +165,7 @@ export class JwsService { alg: protectedJson.alg, }, jwkResolver, + trustedCertificates, }) if (!jwk.supportsSignatureAlgorithm(protectedJson.alg)) { throw new CredoError( @@ -223,9 +227,10 @@ export class JwsService { protectedHeader: { alg: string; [key: string]: unknown } payload: string jwkResolver?: JwsJwkResolver + trustedCertificates?: [string, ...string[]] } ): Promise { - const { protectedHeader, jwkResolver, jws, payload } = options + const { protectedHeader, jwkResolver, jws, payload, trustedCertificates: trustedCertificatesFromOptions } = options if ([protectedHeader.jwk, protectedHeader.kid, protectedHeader.x5c].filter(Boolean).length > 1) { throw new CredoError('Only one of jwk, kid and x5c headers can and must be provided.') @@ -239,16 +244,17 @@ export class JwsService { throw new CredoError('x5c header is not a valid JSON array of string.') } - const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates - if (!trustedCertificates) { + const trustedCertificatesFromConfig = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + const trustedCertificates = [...(trustedCertificatesFromConfig ?? []), ...(trustedCertificatesFromOptions ?? [])] + if (trustedCertificates.length === 0) { throw new CredoError( - 'No trusted certificates configured for X509 certificate chain validation. Issuer cannot be verified.' + `trustedCertificates is required when the JWS protected header contains an 'x5c' property.` ) } await X509Service.validateCertificateChain(agentContext, { certificateChain: protectedHeader.x5c, - trustedCertificates, + trustedCertificates: trustedCertificates as [string, ...string[]], // Already validated that it has at least one certificate }) const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: protectedHeader.x5c }) @@ -308,6 +314,8 @@ export interface VerifyJwsOptions { * base on the `iss` property in the JWT payload. */ jwkResolver?: JwsJwkResolver + + trustedCertificates?: [string, ...string[]] } export type JwsJwkResolver = (options: { diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index befb69dca4..7373d14b00 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -244,7 +244,7 @@ export class DifPresentationExchangeProofFormatService public async processPresentation( agentContext: AgentContext, - { requestAttachment, attachment }: ProofFormatProcessPresentationOptions + { requestAttachment, attachment, proofRecord }: ProofFormatProcessPresentationOptions ): Promise { const ps = this.presentationExchangeService(agentContext) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) @@ -298,6 +298,9 @@ export class DifPresentationExchangeProofFormatService presentation: parsedPresentation, challenge: request.options.challenge, domain: request.options.domain, + verificationContext: { + didcommProofRecordId: proofRecord.id, + }, }) } else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) { if ( diff --git a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts index 3a9b892e89..10e7679016 100644 --- a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts +++ b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts @@ -179,8 +179,22 @@ interface W3cVerifyPresentationOptionsBase { verifyCredentialStatus?: boolean } +export interface VerificationContext { + /** + * The `id` of the `ProofRecord` that this verification is bound to. + */ + didcommProofRecordId?: string + + /** + * The `id` of the `OpenId4VcVerificationSessionRecord` that this verification is bound to. + */ + openId4VcVerificationSessionId?: string +} + export interface W3cJwtVerifyPresentationOptions extends W3cVerifyPresentationOptionsBase { presentation: W3cJwtVerifiablePresentation | string // string must be encoded VP JWT + trustedCertificates?: [string, ...string[]] + verificationContext?: VerificationContext } export interface W3cJsonLdVerifyPresentationOptions extends W3cVerifyPresentationOptionsBase { diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts index 3f4e442b59..fdbd8fb3ab 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts @@ -15,6 +15,7 @@ import { CredoError } from '../../../error' import { injectable } from '../../../plugins' import { asArray, isDid, MessageValidator } from '../../../utils' import { getKeyDidMappingByKeyType, DidResolverService, getKeyFromVerificationMethod } from '../../dids' +import { X509ModuleConfig } from '../../x509' import { W3cJsonLdVerifiableCredential } from '../data-integrity' import { W3cJwtVerifiableCredential } from './W3cJwtVerifiableCredential' @@ -308,6 +309,10 @@ export class W3cJwtCredentialService { const proverPublicKey = getKeyFromVerificationMethod(proverVerificationMethod) const proverPublicJwk = getJwkFromKey(proverPublicKey) + const getTrustedCertificatesForVerification = agentContext.dependencyManager.isRegistered(X509ModuleConfig) + ? agentContext.dependencyManager.resolve(X509ModuleConfig).getTrustedCertificatesForVerification + : undefined + let signatureResult: VerifyJwsResult | undefined = undefined try { // Verify the JWS signature @@ -315,6 +320,9 @@ export class W3cJwtCredentialService { jws: presentation.jwt.serializedJwt, // We have pre-fetched the key based on the singer/holder of the presentation jwkResolver: () => proverPublicJwk, + trustedCertificates: + options.trustedCertificates ?? + (await getTrustedCertificatesForVerification?.(agentContext, options.verificationContext)), }) if (!signatureResult.isValid) { diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts index 72eb5c71f1..e114c5dd7b 100644 --- a/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts @@ -7,6 +7,7 @@ import { getJwkFromKey } from '../../../../crypto/jose/jwk' import { CredoError, ClassValidationError } from '../../../../error' import { JsonTransformer } from '../../../../utils' import { DidJwk, DidKey, DidRepository, DidsModuleConfig } from '../../../dids' +import { X509ModuleConfig } from '../../../x509' import { CREDENTIALS_CONTEXT_V1_URL } from '../../constants' import { ClaimFormat, W3cCredential, W3cPresentation } from '../../models' import { W3cJwtCredentialService } from '../W3cJwtCredentialService' @@ -30,6 +31,7 @@ const agentContext = getAgentContext({ [InjectionSymbols.Logger, testLogger], [DidsModuleConfig, new DidsModuleConfig()], [DidRepository, {} as unknown as DidRepository], + [X509ModuleConfig, new X509ModuleConfig()], ], agentConfig: config, }) diff --git a/packages/core/src/modules/x509/X509ModuleConfig.ts b/packages/core/src/modules/x509/X509ModuleConfig.ts index 5fcd99a076..97ea419393 100644 --- a/packages/core/src/modules/x509/X509ModuleConfig.ts +++ b/packages/core/src/modules/x509/X509ModuleConfig.ts @@ -1,9 +1,25 @@ +import type { AgentContext } from '../../agent' +import type { VerificationContext } from '../vc' + export interface X509ModuleConfigOptions { /** * * Array of trusted base64-encoded certificate strings in the DER-format. */ trustedCertificates?: [string, ...string[]] + + /** + * Optional callback method that will be called to dynamically get trusted certificates for a verification. + * It will always provide the `agentContext` allowing to dynamically set the trusted certificates for a tenant. + * If available the associated record id is also provided allowing to filter down trusted certificates to a single + * exchange. + * + * @returns An array of base64-encoded certificate strings or PEM certificate strings. + */ + getTrustedCertificatesForVerification?( + agentContext: AgentContext, + verificationContext?: VerificationContext + ): Promise<[string, ...string[]] | undefined> } export class X509ModuleConfig { @@ -11,12 +27,21 @@ export class X509ModuleConfig { public constructor(options?: X509ModuleConfigOptions) { this.options = options?.trustedCertificates ? { trustedCertificates: [...options.trustedCertificates] } : {} + this.options.getTrustedCertificatesForVerification = options?.getTrustedCertificatesForVerification } public get trustedCertificates() { return this.options.trustedCertificates } + public get getTrustedCertificatesForVerification() { + return this.options.getTrustedCertificatesForVerification + } + + public setTrustedCertificatesForVerification(fn: X509ModuleConfigOptions['getTrustedCertificatesForVerification']) { + this.options.getTrustedCertificatesForVerification = fn + } + public setTrustedCertificates(trustedCertificates?: [string, ...string[]]) { this.options.trustedCertificates = trustedCertificates ? [...trustedCertificates] : undefined } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 9d4b179c7e..baff7edb7f 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -281,6 +281,7 @@ export class OpenId4VcSiopVerifierService { presentationDefinitions: presentationDefinitionsWithLocation, verification: { presentationVerificationCallback: this.getPresentationVerificationCallback(agentContext, { + correlationId: options.verificationSession.id, nonce: requestNonce, audience: requestClientId, }), @@ -591,7 +592,7 @@ export class OpenId4VcSiopVerifierService { private getPresentationVerificationCallback( agentContext: AgentContext, - options: { nonce: string; audience: string } + options: { nonce: string; audience: string; correlationId: string } ): PresentationVerificationCallback { return async (encodedPresentation, presentationSubmission) => { try { @@ -621,6 +622,9 @@ export class OpenId4VcSiopVerifierService { presentation: encodedPresentation, challenge: options.nonce, domain: options.audience, + verificationContext: { + openId4VcVerificationSessionId: options.correlationId, + }, }) isValid = verificationResult.isValid