diff --git a/.changeset/hip-planes-hunt.md b/.changeset/hip-planes-hunt.md new file mode 100644 index 0000000000..84bd95aad3 --- /dev/null +++ b/.changeset/hip-planes-hunt.md @@ -0,0 +1,5 @@ +--- +"@credo-ts/core": minor +--- + +Remove dependency on `abort-controller` library. Abort Controller has been supported on all platforms for quite some time already. diff --git a/.eslintrc.js b/.eslintrc.js index f20ea4d354..688073a937 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -81,11 +81,6 @@ module.exports = { name: 'Buffer', message: 'Global buffer is not supported on all platforms. Import buffer from `src/utils/buffer`', }, - { - name: 'AbortController', - message: - "Global AbortController is not supported on all platforms. Use `import { AbortController } from 'abort-controller'`", - }, ], }, }, diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsIssuerService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsIssuerService.ts index 9636c1c4ea..1e0cb1a589 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsIssuerService.ts @@ -320,7 +320,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } let revocationConfiguration: CredentialRevocationConfig | undefined - if (revocationRegistryDefinitionId && revocationStatusList && revocationRegistryIndex) { + if (revocationRegistryDefinitionId && revocationStatusList && revocationRegistryIndex !== undefined) { const revocationRegistryDefinitionRecord = await agentContext.dependencyManager .resolve(AnonCredsRevocationRegistryDefinitionRepository) .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 103c7174fb..2d176d895b 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -567,7 +567,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService // if the proposal has an attachment Id use that, otherwise the generated id of the formats object const format = new CredentialFormatSpec({ attachmentId: attachmentId, - format: ANONCREDS_CREDENTIAL, + format: ANONCREDS_CREDENTIAL_OFFER, }) const offer = await anonCredsIssuerService.createCredentialOffer(agentContext, { diff --git a/packages/core/package.json b/packages/core/package.json index 82033c7eeb..7545519551 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,7 +47,6 @@ "@sphereon/ssi-types": "0.30.2-next.135", "@stablelib/ed25519": "^1.0.2", "@types/ws": "^8.5.4", - "abort-controller": "^3.0.0", "big-integer": "^1.6.51", "borc": "^3.0.0", "buffer": "^6.0.3", diff --git a/packages/core/src/transport/HttpOutboundTransport.ts b/packages/core/src/transport/HttpOutboundTransport.ts index 72d34d11bb..0d68d21413 100644 --- a/packages/core/src/transport/HttpOutboundTransport.ts +++ b/packages/core/src/transport/HttpOutboundTransport.ts @@ -4,7 +4,6 @@ import type { AgentMessageReceivedEvent } from '../agent/Events' import type { Logger } from '../logger' import type { OutboundPackage } from '../types' -import { AbortController } from 'abort-controller' import { Subject } from 'rxjs' import { AgentEventTypes } from '../agent/Events' diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts index 1c2f842de3..29f4acdece 100644 --- a/packages/core/src/utils/fetch.ts +++ b/packages/core/src/utils/fetch.ts @@ -1,7 +1,5 @@ import type { AgentDependencies } from '../agent/AgentDependencies' -import { AbortController } from 'abort-controller' - export async function fetchWithTimeout( fetch: AgentDependencies['fetch'], url: string, diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index cbe8574983..5ba6dcfb81 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -1,6 +1,5 @@ import type { AgentDependencies } from '../agent/AgentDependencies' -import { AbortController } from 'abort-controller' import { parseUrl } from 'query-string' import { AgentMessage } from '../agent/AgentMessage' diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts index 5dfd6b0591..0adcb8b5f4 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -4,10 +4,11 @@ import type { AgentContext } from '@credo-ts/core' import type { AuthorizationResponsePayload, DecryptCompact } from '@sphereon/did-auth-siop' import type { Response, Router } from 'express' +import { Oauth2ErrorCodes, Oauth2ServerErrorResponseError } from '@animo-id/oauth2' import { CredoError, Hasher, JsonEncoder, Key, TypedArrayEncoder } from '@credo-ts/core' import { AuthorizationRequest, RP } from '@sphereon/did-auth-siop' -import { getRequestContext, sendErrorResponse, sendJsonResponse } from '../../shared/router' +import { getRequestContext, sendErrorResponse, sendJsonResponse, sendOauth2ErrorResponse } from '../../shared/router' import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' export interface OpenId4VcSiopAuthorizationEndpointConfig { @@ -68,6 +69,8 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => { const { agentContext, verifier } = getRequestContext(request) + let jarmResponseType: string | undefined + try { const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) @@ -95,6 +98,8 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc hasher: Hasher.hash, }) + jarmResponseType = res.type + const [header] = request.body.response.split('.') jarmHeader = JsonEncoder.fromBase64(header) // FIXME: verify the apv matches the nonce of the authorization reuqest @@ -123,6 +128,29 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc throw new CredoError('Missing verification session, cannot verify authorization response.') } + const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(verificationSession.authorizationRequestJwt) + const response_mode = await authorizationRequest.getMergedProperty('response_mode') + if (response_mode?.includes('jwt') && !jarmResponseType) { + throw new Oauth2ServerErrorResponseError({ + error: Oauth2ErrorCodes.InvalidRequest, + error_description: `JARM response is required for JWT response mode '${response_mode}'.`, + }) + } + + if (!response_mode?.includes('jwt') && jarmResponseType) { + throw new Oauth2ServerErrorResponseError({ + error: Oauth2ErrorCodes.InvalidRequest, + error_description: `Recieved JARM response which is incompatible with response mode '${response_mode}'.`, + }) + } + + if (jarmResponseType && jarmResponseType !== 'encrypted') { + throw new Oauth2ServerErrorResponseError({ + error: Oauth2ErrorCodes.InvalidRequest, + error_description: `Only encrypted JARM responses are supported, received '${jarmResponseType}'.`, + }) + } + await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, { authorizationResponse: authorizationResponsePayload, verificationSession, @@ -133,6 +161,10 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc presentation_during_issuance_session: verificationSession.presentationDuringIssuanceSession, }) } catch (error) { + if (error instanceof Oauth2ServerErrorResponseError) { + return sendOauth2ErrorResponse(response, next, agentContext.config.logger, error) + } + return sendErrorResponse(response, next, agentContext.config.logger, 500, 'invalid_request', error) } }) diff --git a/packages/openid4vc/src/shared/router/context.ts b/packages/openid4vc/src/shared/router/context.ts index 81890bb635..cc383d693f 100644 --- a/packages/openid4vc/src/shared/router/context.ts +++ b/packages/openid4vc/src/shared/router/context.ts @@ -83,7 +83,7 @@ export function sendErrorResponse( error: unknown, additionalPayload?: Record ) { - const body = { error: message, ...additionalPayload } + const body = { error: message, ...(error instanceof Error && { cause: error.message }), ...additionalPayload } logger.warn(`[OID4VC] Sending error response: ${JSON.stringify(body)}`, { error, }) diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index b4bb7753c2..edd125832b 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -37,6 +37,7 @@ import { JwsService, JwtPayload, } from '@credo-ts/core' +import { ResponseMode } from '@sphereon/did-auth-siop' import express, { type Express } from 'express' import { AskarModule } from '../../askar/src' @@ -1857,6 +1858,112 @@ describe('OpenId4Vc', () => { await holderTenant1.endSession() }) + it('e2e flow with verifier endpoints verifying a mdoc fails without direct_post.jwt', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + + const selfSignedCertificate = await X509Service.createSelfSignedCertificate(issuer.agent.context, { + key: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + extensions: [], + name: 'C=DE', + }) + + await verifier.agent.x509.setTrustedCertificates([selfSignedCertificate.toString('pem')]) + + const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) + const signedMdoc = await issuer.agent.mdoc.sign({ + docType: 'org.eu.university', + holderKey, + issuerCertificate: selfSignedCertificate.toString('pem'), + namespaces: { + 'eu.europa.ec.eudi.pid.1': { + university: 'innsbruck', + degree: 'bachelor', + name: 'John Doe', + not: 'disclosed', + }, + }, + }) + + 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.mdoc.store(signedMdoc) + + await holder.agent.x509.addTrustedCertificate(rawCertificate) + await verifier.agent.x509.addTrustedCertificate(rawCertificate) + + const presentationDefinition = { + id: 'mDL-sample-req', + input_descriptors: [ + { + id: 'org.eu.university', + format: { + mso_mdoc: { + alg: ['ES256', 'ES384', 'ES512', 'EdDSA', 'ESB256', 'ESB320', 'ESB384', 'ESB512'], + }, + }, + constraints: { + fields: [ + { + path: ["$['eu.europa.ec.eudi.pid.1']['name']"], + intent_to_retain: false, + }, + { + path: ["$['eu.europa.ec.eudi.pid.1']['degree']"], + intent_to_retain: false, + }, + ], + limit_disclosure: 'required', + }, + }, + ], + } satisfies DifPresentationExchangeDefinitionV2 + + const { authorizationRequest } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + responseMode: 'direct_post.jwt', + verifierId: openIdVerifier.verifierId, + requestSigner: { + method: 'x5c', + x5c: [rawCertificate], + issuer: 'https://example.com/hakuna/matadata', + }, + presentationExchange: { definition: presentationDefinition }, + }) + + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequest + ) + + if (!resolvedAuthorizationRequest.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + const selectedCredentials = holder.agent.modules.openId4VcHolder.selectCredentialsForRequest( + resolvedAuthorizationRequest.presentationExchange.credentialsForRequest + ) + + const requestPayload = + await resolvedAuthorizationRequest.authorizationRequest.authorizationRequest.requestObject?.getPayload() + if (!requestPayload) { + throw new Error('No payload') + } + + // setting this to direct_post to simulate the result of sending a non encrypted response to an authorization request that requires enryption + requestPayload.response_mode = ResponseMode.DIRECT_POST + + await expect( + holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + ).rejects.toThrow(/JARM response is required/) + }) + it('e2e flow with verifier endpoints verifying a mdoc and sd-jwt (jarm)', async () => { const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07e820f4cd..c40e72e73d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -505,9 +505,6 @@ importers: '@types/ws': specifier: ^8.5.4 version: 8.5.12 - abort-controller: - specifier: ^3.0.0 - version: 3.0.0 big-integer: specifier: ^1.6.51 version: 1.6.52 @@ -7406,9 +7403,11 @@ packages: sudo-prompt@8.2.5: resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. sudo-prompt@9.1.1: resolution: {integrity: sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. sudo-prompt@9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} @@ -14736,7 +14735,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -14748,7 +14747,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -14769,7 +14768,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3