Skip to content

Commit

Permalink
fix: dcql alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Nov 21, 2024
1 parent dc1c318 commit 6ff3355
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,7 @@ describe('presentation exchange manager tests', () => {
const payload = await getPayloadVID1Val()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(payload.claims?.vp_token as any).presentation_definition_uri = EXAMPLE_PD_URL
await expect(PresentationExchange.findValidPresentationDefinitions(payload)).rejects.toThrow(
SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_BY_REF_AND_VALUE_NON_EXCLUSIVE,
)
await expect(PresentationExchange.findValidPresentationDefinitions(payload)).rejects.toThrow(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE)
})

it('validatePresentationAgainstDefinition: should pass if provided VP match the PD', async function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CredentialMapper, Hasher, WrappedVerifiablePresentation } from '@sphereon/ssi-types'
import { DcqlPresentationRecord } from 'dcql'

import { AuthorizationRequest, VerifyAuthorizationRequestOpts } from '../authorization-request'
import { assertValidVerifyAuthorizationRequestOpts } from '../authorization-request/Opts'
import { IDToken } from '../id-token'
import { AuthorizationResponsePayload, ResponseType, SIOPErrors, VerifiedAuthorizationRequest, VerifiedAuthorizationResponse } from '../types'

import { assertValidDcqlPresentationRecrod } from './Dcql'
import {
assertValidVerifiablePresentations,
extractNonceFromWrappedVerifiablePresentation,
Expand Down Expand Up @@ -142,7 +144,13 @@ export class AuthorizationResponse {
},
})
} else {
console.error('TODO: VALIDATE PRESENTATION AGAINST DEFINITION')
const dcqlQuery = verifiedAuthorizationRequest.dcqlQuery
if (!dcqlQuery) {
throw new Error('vp_token is present, but no presentation definitions or dcql query provided')
}
assertValidDcqlPresentationRecrod(responseOpts.dcqlQuery.encodedPresentationRecord as DcqlPresentationRecord, dcqlQuery, {
hasher: verifyOpts.hasher,
})
}
}

Expand All @@ -160,7 +168,16 @@ export class AuthorizationResponse {
}

const verifiedIdToken = await this.idToken?.verify(verifyOpts)
const oid4vp = await verifyPresentations(this, verifyOpts)
if (this.payload.vp_token && !verifyOpts.presentationDefinitions && !verifyOpts.dcqlQuery) {
throw Promise.reject(Error('vp_token is present, but no presentation definitions or dcql query provided'))
}

const emptyPresentationDefinitions = Array.isArray(verifyOpts.presentationDefinitions) && verifyOpts.presentationDefinitions.length === 0
if (!this.payload.vp_token && ((verifyOpts.presentationDefinitions && !emptyPresentationDefinitions) || verifyOpts.dcqlQuery)) {
throw Promise.reject(Error('Presentation definitions or dcql query provided, but no vp_token present'))
}

const oid4vp = this.payload.vp_token ? await verifyPresentations(this, verifyOpts) : undefined

// Gather all nonces
const allNonces = new Set<string>()
Expand Down Expand Up @@ -227,7 +244,7 @@ export class AuthorizationResponse {
presentations = extractPresentationsFromVpToken(this._payload.vp_token, opts)
}

if (presentations && Array.isArray(presentations) && presentations.length === 0) {
if (!presentations || (Array.isArray(presentations) && presentations.length === 0)) {
return Promise.reject(Error('missing presentation(s)'))
}
const presentationsArray = Array.isArray(presentations) ? presentations : [presentations]
Expand Down
33 changes: 23 additions & 10 deletions packages/siop-oid4vp/lib/authorization-response/Dcql.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { DcqlQuery } from 'dcql'
import { Hasher } from '@sphereon/ssi-types'
import { DcqlMdocRepresentation, DcqlPresentationRecord, DcqlQuery, DcqlSdJwtVcRepresentation } from 'dcql'

import { extractDataFromPath } from '../helpers'
import { AuthorizationRequestPayload, SIOPErrors } from '../types'

import { extractPresentationRecordFromDcqlVpToken } from './OpenID4VP'

/**
* Finds a valid DcqlQuery inside the given AuthenticationRequestPayload
* throws exception if the DcqlQuery is not valid
Expand All @@ -12,29 +15,39 @@ import { AuthorizationRequestPayload, SIOPErrors } from '../types'
*/
export const findValidDcqlQuery = async (authorizationRequestPayload: AuthorizationRequestPayload): Promise<DcqlQuery | undefined> => {
const dcqlQuery: string[] = extractDataFromPath(authorizationRequestPayload, '$.dcql_query').map((d) => d.value)
const dcqlQueryList: string[] = extractDataFromPath(authorizationRequestPayload, '$.dcql_query[*]').map((d) => d.value)
const definitions = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition')
const definitionsFromList = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition[*]')
const definitionRefs = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition_uri')
const definitionRefsFromList = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition_uri[*]')

const hasPD = (definitions && definitions.length > 0) || (definitionsFromList && definitionsFromList.length > 0)
const hasPdRef = (definitionRefs && definitionRefs.length > 0) || (definitionRefsFromList && definitionRefsFromList.length > 0)
const hasDcql = (dcqlQuery && dcqlQuery.length > 0) || (dcqlQueryList && dcqlQueryList.length > 0)
const hasDcql = dcqlQuery && dcqlQuery.length > 0

if ([hasPD, hasPdRef, hasDcql].filter(Boolean).length > 1) {
throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE)
}

if (dcqlQuery.length > 1 || dcqlQueryList.length > 1) {
if (dcqlQuery.length === 0) return undefined

if (dcqlQuery.length > 1) {
throw new Error('Found multiple dcql_query in vp_token. Only one is allowed')
}

const encoded = dcqlQuery.length ? dcqlQuery[0] : dcqlQueryList[0]
if (!encoded) return undefined

const parsedDcqlQuery = DcqlQuery.parse(JSON.parse(encoded))
DcqlQuery.validate(parsedDcqlQuery)
return DcqlQuery.parse(JSON.parse(dcqlQuery[0]))
}

return parsedDcqlQuery
export const assertValidDcqlPresentationRecrod = async (record: DcqlPresentationRecord | string, dcqlQuery: DcqlQuery, opts: { hasher?: Hasher }) => {
const wrappedPresentations = Object.values(extractPresentationRecordFromDcqlVpToken(record, opts))
const credentials = wrappedPresentations.map((p) => {
if (p.format === 'mso_mdoc') {
return { docType: p.vcs[0].credential.toJson().docType, namespaces: p.vcs[0].decoded } satisfies DcqlMdocRepresentation
} else if (p.format === 'vc+sd-jwt') {
return { vct: p.vcs[0].decoded.vct, claims: p.vcs[0].decoded } satisfies DcqlSdJwtVcRepresentation
} else {
throw new Error('DcqlPresentation atm only supports mso_mdoc and vc+sd-jwt')
}
})

DcqlPresentationRecord.validate(credentials, { dcqlQuery })
}
35 changes: 19 additions & 16 deletions packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
W3CVerifiablePresentation,
WrappedVerifiablePresentation,
} from '@sphereon/ssi-types'
import { DcqlQuery, DcqlVpToken } from 'dcql'
import { DcqlPresentationRecord, DcqlQuery } from 'dcql'

import { AuthorizationRequest } from '../authorization-request'
import { verifyRevocation } from '../helpers'
Expand All @@ -26,6 +26,7 @@ import {
} from '../types'

import { AuthorizationResponse } from './AuthorizationResponse'
import { assertValidDcqlPresentationRecrod } from './Dcql'
import { PresentationExchange } from './PresentationExchange'
import {
AuthorizationResponseOpts,
Expand Down Expand Up @@ -72,15 +73,6 @@ export const verifyPresentations = async (
authorizationResponse: AuthorizationResponse,
verifyOpts: VerifyAuthorizationResponseOpts,
): Promise<VerifiedOpenID4VPSubmission | VerifiedOpenID4VPSubmissionDcql | null> => {
if (!authorizationResponse.payload.vp_token) return null
if (
authorizationResponse.payload.vp_token &&
Array.isArray(authorizationResponse.payload.vp_token) &&
authorizationResponse.payload.vp_token.length === 0
) {
return Promise.reject(Error('the payload is missing a vp_token'))
}

let idPayload: IDTokenPayload | undefined
if (authorizationResponse.idToken) {
idPayload = await authorizationResponse.idToken.payload()
Expand All @@ -98,8 +90,6 @@ export const verifyPresentations = async (
let dcqlQuery = verifyOpts.dcqlQuery ?? authorizationResponse?.authorizationRequest?.payload.dcql_query
if (dcqlQuery) {
dcqlQuery = DcqlQuery.parse(dcqlQuery)
DcqlQuery.validate(dcqlQuery)

wrappedPresentations = extractPresentationsFromDcqlVpToken(authorizationResponse.payload.vp_token as string, { hasher: verifyOpts.hasher })

const verifiedPresentations = await Promise.all(
Expand All @@ -108,7 +98,7 @@ export const verifyPresentations = async (
),
)

// TODO: assert the submission against the definition
assertValidDcqlPresentationRecrod(authorizationResponse.payload.vp_token as string, dcqlQuery, { hasher: verifyOpts.hasher })

if (verifiedPresentations.some((verified) => !verified)) {
const message = verifiedPresentations
Expand Down Expand Up @@ -170,12 +160,25 @@ export const verifyPresentations = async (
}
}

export const extractPresentationRecordFromDcqlVpToken = (
vpToken: DcqlPresentationRecord.Input | string,
opts?: { hasher?: Hasher },
): Record<string, WrappedVerifiablePresentation> => {
const presentationRecord = Object.fromEntries(
Object.entries(DcqlPresentationRecord.parse(vpToken)).map(([credentialQueryId, vp]) => [
credentialQueryId,
CredentialMapper.toWrappedVerifiablePresentation(vp as W3CVerifiablePresentation | CompactSdJwtVc | string, { hasher: opts.hasher }),
]),
)

return presentationRecord
}

export const extractPresentationsFromDcqlVpToken = (
vpToken: DcqlVpToken.Input | string,
vpToken: DcqlPresentationRecord.Input | string,
opts?: { hasher?: Hasher },
): WrappedVerifiablePresentation[] => {
const presentations = Object.values(DcqlVpToken.parse(vpToken)) as Array<W3CVerifiablePresentation | CompactSdJwtVc | string>
return presentations.map((vp) => CredentialMapper.toWrappedVerifiablePresentation(vp, { hasher: opts.hasher }))
return Object.values(extractPresentationRecordFromDcqlVpToken(vpToken, opts))
}

export const extractPresentationsFromVpToken = (
Expand Down
4 changes: 3 additions & 1 deletion packages/siop-oid4vp/lib/authorization-response/Payload.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DcqlPresentationRecord } from 'dcql'

import { AuthorizationRequest } from '../authorization-request'
import { IDToken } from '../id-token'
import { RequestObject } from '../request-object'
Expand Down Expand Up @@ -29,7 +31,7 @@ export const createResponsePayload = async (

// vp tokens
if (responseOpts.dcqlQuery) {
responsePayload.vp_token = JSON.stringify(responseOpts.dcqlQuery.credentialQueryIdToPresentation)
responsePayload.vp_token = DcqlPresentationRecord.encode(responseOpts.dcqlQuery.encodedPresentationRecord as DcqlPresentationRecord)
} else {
await putPresentationSubmissionInLocation(authorizationRequest, responsePayload, responseOpts, idTokenPayload)
}
Expand Down
6 changes: 1 addition & 5 deletions packages/siop-oid4vp/lib/authorization-response/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ export interface PresentationExchangeResponseOpts {
}

export interface DcqlQueryResponseOpts {
credentialQueryIdToPresentation: Record<string, Record<string, unknown> | string>
}

export interface PresentationExchangeRequestOpts {
presentationVerificationCallback?: PresentationVerificationCallback
encodedPresentationRecord: Record<string, Record<string, unknown> | string>
}

export interface PresentationDefinitionPayloadOpts {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2342,7 +2342,7 @@ export const AuthorizationResponseOptsSchemaObj = {
"DcqlQueryResponseOpts": {
"type": "object",
"properties": {
"credentialQueryIdToPresentation": {
"encodedPresentationRecord": {
"type": "object",
"additionalProperties": {
"anyOf": [
Expand All @@ -2358,7 +2358,7 @@ export const AuthorizationResponseOptsSchemaObj = {
}
},
"required": [
"credentialQueryIdToPresentation"
"encodedPresentationRecord"
],
"additionalProperties": false
}
Expand Down
4 changes: 2 additions & 2 deletions packages/siop-oid4vp/lib/types/SIOP.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export interface IDTokenPayload extends JWTPayload {
}
}

export type DcqlQueryVpToken = string
export type EcodedDcqlQueryVpToken = string

export interface AuthorizationResponsePayload {
access_token?: string
Expand All @@ -181,7 +181,7 @@ export interface AuthorizationResponsePayload {
| W3CVerifiablePresentation
| CompactSdJwtVc
| MdocOid4vpMdocVpToken
| DcqlQueryVpToken
| EcodedDcqlQueryVpToken
presentation_submission?: PresentationSubmission
verifiedData?: IPresentation | AdditionalClaims
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
2 changes: 1 addition & 1 deletion packages/siop-oid4vp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@sphereon/jarm": "workspace:*",
"@sphereon/oid4vc-common": "workspace:*",
"@sphereon/pex": "5.0.0-unstable.24",
"dcql": "link:../../../dcql/dcql",
"dcql": "^0.2.7",
"@sphereon/pex-models": "^2.3.1",
"@sphereon/ssi-types": "0.30.2-next.129",
"cross-fetch": "^4.0.0",
Expand Down
25 changes: 23 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6ff3355

Please sign in to comment.