Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: assert valid jarm response #2117

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string>('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,
Expand All @@ -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)
}
})
Expand Down
2 changes: 1 addition & 1 deletion packages/openid4vc/src/shared/router/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function sendErrorResponse(
error: unknown,
additionalPayload?: Record<string, unknown>
) {
const body = { error: message, ...additionalPayload }
const body = { error: message, ...(error instanceof Error && { cause: error.message }), ...additionalPayload }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not compliant with the spec. there is error and error_description. But we should be cautious with just returning the error message for unknown errors.

For oid4vci i reworked it to now throw a Oauth2ServerErrorResponseError and in the place where sendErrorResponse is called i first check for instanceof Oauth2ServerErrorResponseError and then call sendOauth2ErrorResponse. Then we make sure only error messages that should be leaked to the client are leaked. (Because we only construct a Oauth2ServerErrorResponseError if needed. Can you udpate?

logger.warn(`[OID4VC] Sending error response: ${JSON.stringify(body)}`, {
error,
})
Expand Down
107 changes: 107 additions & 0 deletions packages/openid4vc/tests/openid4vc.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()

Expand Down
Loading