Skip to content

Commit

Permalink
feat: dcql alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Nov 21, 2024
1 parent 4b7e8ae commit dc1c318
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CredentialMapper, Hasher } from '@sphereon/ssi-types'
import { CredentialMapper, Hasher, WrappedVerifiablePresentation } from '@sphereon/ssi-types'

import { AuthorizationRequest, VerifyAuthorizationRequestOpts } from '../authorization-request'
import { assertValidVerifyAuthorizationRequestOpts } from '../authorization-request/Opts'
Expand All @@ -11,6 +11,7 @@ import {
extractPresentationsFromVpToken,
verifyPresentations,
} from './OpenID4VP'
import { extractPresentationsFromDcqlVpToken } from './OpenID4VP'
import { assertValidResponseOpts } from './Opts'
import { createResponsePayload } from './Payload'
import { AuthorizationResponseOpts, PresentationDefinitionWithLocation, VerifyAuthorizationResponseOpts } from './types'
Expand Down Expand Up @@ -141,7 +142,7 @@ export class AuthorizationResponse {
},
})
} else {
throw new Error('TODO: VALIDATE PRESENTATION AGAINST DEFINITION')
console.error('TODO: VALIDATE PRESENTATION AGAINST DEFINITION')
}
}

Expand Down Expand Up @@ -189,7 +190,8 @@ export class AuthorizationResponse {
state,
correlationId: verifyOpts.correlationId,
...(this.idToken && { idToken: verifiedIdToken }),
...(oid4vp && { oid4vpSubmission: oid4vp }),
...(oid4vp && 'presentationDefinitions' in oid4vp && { oid4vpSubmission: oid4vp }),
...(oid4vp && 'dcqlQuery' in oid4vp && { oid4vpSubmissionDcql: oid4vp }),
}
}

Expand Down Expand Up @@ -217,8 +219,15 @@ export class AuthorizationResponse {
public async mergedPayloads(opts?: { consistencyCheck?: boolean; hasher?: Hasher }): Promise<AuthorizationResponsePayload> {
let nonce: string | undefined = this._payload.nonce
if (this._payload?.vp_token) {
const presentations = this.payload.vp_token ? await extractPresentationsFromVpToken(this.payload.vp_token, opts) : []
if (!presentations || (Array.isArray(presentations) && presentations.length === 0)) {
let presentations: WrappedVerifiablePresentation | WrappedVerifiablePresentation[]

try {
presentations = extractPresentationsFromDcqlVpToken(this._payload.vp_token as string, opts)
} catch (e) {
presentations = extractPresentationsFromVpToken(this._payload.vp_token, opts)
}

if (presentations && Array.isArray(presentations) && presentations.length === 0) {
return Promise.reject(Error('missing presentation(s)'))
}
const presentationsArray = Array.isArray(presentations) ? presentations : [presentations]
Expand Down
20 changes: 8 additions & 12 deletions packages/siop-oid4vp/lib/authorization-response/Dcql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,30 @@ import { AuthorizationRequestPayload, SIOPErrors } from '../types'
* @param version
*/
export const findValidDcqlQuery = async (authorizationRequestPayload: AuthorizationRequestPayload): Promise<DcqlQuery | undefined> => {
const vpTokens: string[] = extractDataFromPath(authorizationRequestPayload, '$..vp_token.dcql_query').map((d) => d.value)
const vpTokensList: string[] = extractDataFromPath(authorizationRequestPayload, '$..vp_token.dcql_query[*]').map((d) => d.value)
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 = (vpTokens && vpTokens.length > 0) || (vpTokensList && vpTokensList.length > 0)
const hasDcql = (dcqlQuery && dcqlQuery.length > 0) || (dcqlQueryList && dcqlQueryList.length > 0)

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

if (!(vpTokens && vpTokens.length) && !(vpTokensList && vpTokensList.length)) {
throw new Error('Cannot find dcql_query in vp_token. Presentation definition is present')
}

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

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

const dcqlQuery = DcqlQuery.parse(JSON.parse(encoded))
DcqlQuery.validate(dcqlQuery)
const parsedDcqlQuery = DcqlQuery.parse(JSON.parse(encoded))
DcqlQuery.validate(parsedDcqlQuery)

return dcqlQuery
return parsedDcqlQuery
}
62 changes: 37 additions & 25 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, DcqlQueryVpToken } from 'dcql'
import { DcqlQuery, DcqlVpToken } from 'dcql'

import { AuthorizationRequest } from '../authorization-request'
import { verifyRevocation } from '../helpers'
Expand All @@ -22,6 +22,7 @@ import {
SIOPErrors,
SupportedVersion,
VerifiedOpenID4VPSubmission,
VerifiedOpenID4VPSubmissionDcql,
} from '../types'

import { AuthorizationResponse } from './AuthorizationResponse'
Expand Down Expand Up @@ -70,10 +71,12 @@ export const extractNonceFromWrappedVerifiablePresentation = (wrappedVp: Wrapped
export const verifyPresentations = async (
authorizationResponse: AuthorizationResponse,
verifyOpts: VerifyAuthorizationResponseOpts,
): Promise<VerifiedOpenID4VPSubmission | null> => {
): 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)
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'))
}
Expand All @@ -83,28 +86,26 @@ export const verifyPresentations = async (
idPayload = await authorizationResponse.idToken.payload()
}

// todo: Probably wise to check against request for the location of the submission_data
const presentationSubmission = idPayload?._vp_token?.presentation_submission ?? authorizationResponse.payload.presentation_submission

let wrappedPresentations: WrappedVerifiablePresentation[] = []
const presentationDefinitions = verifyOpts.presentationDefinitions
? Array.isArray(verifyOpts.presentationDefinitions)
? verifyOpts.presentationDefinitions
: [verifyOpts.presentationDefinitions]
: []

const dcqlQuery = verifyOpts.dcqlQuery ?? authorizationResponse.authorizationRequest.payload.dcql_query
if (dcqlQuery) {
const dcqlQueryVpToken = DcqlQueryVpToken.parse(JSON.parse(authorizationResponse.payload.vp_token as string))
let presentationSubmission: PresentationSubmission | undefined

const parsedQuery = DcqlQuery.parse(dcqlQuery)
DcqlQuery.validate(parsedQuery)
let dcqlQuery = verifyOpts.dcqlQuery ?? authorizationResponse?.authorizationRequest?.payload.dcql_query
if (dcqlQuery) {
dcqlQuery = DcqlQuery.parse(dcqlQuery)
DcqlQuery.validate(dcqlQuery)

const presentations = Object.values(dcqlQueryVpToken) as Array<W3CVerifiablePresentation | CompactSdJwtVc | string>
wrappedPresentations = presentations.map((vp) => CredentialMapper.toWrappedVerifiablePresentation(vp, { hasher: verifyOpts.hasher }))
wrappedPresentations = extractPresentationsFromDcqlVpToken(authorizationResponse.payload.vp_token as string, { hasher: verifyOpts.hasher })

const verifiedPresentations = await Promise.all(
presentations.map((presentation) => verifyOpts.verification.presentationVerificationCallback(presentation, presentationSubmission)),
wrappedPresentations.map((presentation) =>
verifyOpts.verification.presentationVerificationCallback(presentation.original as W3CVerifiablePresentation),
),
)

// TODO: assert the submission against the definition
Expand All @@ -118,9 +119,14 @@ export const verifyPresentations = async (
throw Error(`Failed to verify presentations. ${message}`)
}
} else {
const presentations = await extractPresentationsFromVpToken(authorizationResponse.payload.vp_token, { hasher: verifyOpts.hasher })
const presentations = authorizationResponse.payload.vp_token
? await extractPresentationsFromVpToken(authorizationResponse.payload.vp_token, { hasher: verifyOpts.hasher })
: []
wrappedPresentations = Array.isArray(presentations) ? presentations : [presentations]

// todo: Probably wise to check against request for the location of the submission_data
presentationSubmission = idPayload?._vp_token?.presentation_submission ?? authorizationResponse.payload.presentation_submission

await assertValidVerifiablePresentations({
presentationDefinitions,
presentations,
Expand All @@ -134,12 +140,6 @@ export const verifyPresentations = async (
})
}

// If there are no presentations, and the `assertValidVerifiablePresentations` did not fail
// it means there's no oid4vp response and also not requested
if (wrappedPresentations.length === 0) {
return null
}

const presentationsWithoutMdoc = wrappedPresentations.filter((p) => p.format !== 'mso_mdoc')
const nonces = new Set(presentationsWithoutMdoc.map(extractNonceFromWrappedVerifiablePresentation))
if (presentationsWithoutMdoc.length > 0 && nonces.size !== 1) {
Expand All @@ -163,13 +163,25 @@ export const verifyPresentations = async (
await verifyRevocation(vp, verifyOpts.verification.revocationOpts.revocationVerificationCallback, revocationVerification)
}
}
return { nonce, presentations: wrappedPresentations, presentationDefinitions, submissionData: presentationSubmission }
if (presentationDefinitions) {
return { nonce, presentations: wrappedPresentations, presentationDefinitions, submissionData: presentationSubmission }
} else {
return { nonce, presentations: wrappedPresentations, dcqlQuery }
}
}

export const extractPresentationsFromDcqlVpToken = (
vpToken: DcqlVpToken.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 }))
}

export const extractPresentationsFromVpToken = async (
export const extractPresentationsFromVpToken = (
vpToken: Array<W3CVerifiablePresentation | CompactSdJwtVc | string> | W3CVerifiablePresentation | CompactSdJwtVc | string,
opts?: { hasher?: Hasher },
): Promise<WrappedVerifiablePresentation[] | WrappedVerifiablePresentation> => {
): WrappedVerifiablePresentation[] | WrappedVerifiablePresentation => {
const tokens = Array.isArray(vpToken) ? vpToken : [vpToken]
const wrappedTokens = tokens.map((vp) => CredentialMapper.toWrappedVerifiablePresentation(vp, { hasher: opts?.hasher }))

Expand Down
6 changes: 5 additions & 1 deletion packages/siop-oid4vp/lib/authorization-response/Payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export const createResponsePayload = async (
}

// vp tokens
await putPresentationSubmissionInLocation(authorizationRequest, responsePayload, responseOpts, idTokenPayload)
if (responseOpts.dcqlQuery) {
responsePayload.vp_token = JSON.stringify(responseOpts.dcqlQuery.credentialQueryIdToPresentation)
} else {
await putPresentationSubmissionInLocation(authorizationRequest, responsePayload, responseOpts, idTokenPayload)
}
if (idTokenPayload) {
const idToken = await IDToken.fromIDTokenPayload(idTokenPayload, responseOpts)
responsePayload.id_token = await idToken.jwt(responseOpts.jwtIssuer)
Expand Down
6 changes: 3 additions & 3 deletions packages/siop-oid4vp/lib/authorization-response/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PresentationSubmission,
W3CVerifiablePresentation,
} from '@sphereon/ssi-types'
import { DcqlQuery, DcqlQueryVpToken } from 'dcql'
import { DcqlQuery } from 'dcql'

import {
ResponseMode,
Expand Down Expand Up @@ -62,7 +62,7 @@ export interface PresentationExchangeResponseOpts {
}

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

export interface PresentationExchangeRequestOpts {
Expand Down Expand Up @@ -108,7 +108,7 @@ export type PresentationVerificationResult = { verified: boolean; reason?: strin

export type PresentationVerificationCallback = (
args: W3CVerifiablePresentation | CompactSdJwtVc | MdocOid4vpIssuerSigned,
presentationSubmission: PresentationSubmission,
presentationSubmission?: PresentationSubmission,
) => Promise<PresentationVerificationResult>

export type PresentationSignCallback = (args: PresentationSignCallBackParams) => Promise<W3CVerifiablePresentation | CompactSdJwtVc>
Expand Down
6 changes: 5 additions & 1 deletion packages/siop-oid4vp/lib/rp/RP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { mergeVerificationOpts } from '../authorization-request/Opts'
import {
AuthorizationResponse,
extractPresentationsFromDcqlVpToken,
extractPresentationsFromVpToken,
PresentationDefinitionWithLocation,
VerifyAuthorizationResponseOpts,
Expand Down Expand Up @@ -169,7 +170,10 @@ export class RP {
},
)

const presentations = await extractPresentationsFromVpToken(validatedResponse.authResponseParams.vp_token, { hasher })
const presentations = validatedResponse.authRequestParams.dcql_query
? extractPresentationsFromDcqlVpToken(validatedResponse.authResponseParams.vp_token as string, { hasher })
: extractPresentationsFromVpToken(validatedResponse.authResponseParams.vp_token, { hasher })

const mdocVerifiablePresentations = (Array.isArray(presentations) ? presentations : [presentations]).filter((p) => p.format === 'mso_mdoc')

if (mdocVerifiablePresentations.length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ export const AuthorizationRequestPayloadVD12OID4VPD20SchemaObj = {
},
"response_uri": {
"type": "string"
},
"dcql_query": {
"type": "string"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ export const AuthorizationRequestPayloadVID1SchemaObj = {
},
"presentation_definition_uri": {
"type": "string"
},
"dcql_query": {
"type": "string"
}
},
"additionalProperties": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export const AuthorizationResponseOptsSchemaObj = {
},
"presentationExchange": {
"$ref": "#/definitions/PresentationExchangeResponseOpts"
},
"dcqlQuery": {
"$ref": "#/definitions/DcqlQueryResponseOpts"
}
},
"required": [
Expand Down Expand Up @@ -2335,6 +2338,29 @@ export const AuthorizationResponseOptsSchemaObj = {
"id_token",
"token_response"
]
},
"DcqlQueryResponseOpts": {
"type": "object",
"properties": {
"credentialQueryIdToPresentation": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "object",
"additionalProperties": {}
},
{
"type": "string"
}
]
}
}
},
"required": [
"credentialQueryIdToPresentation"
],
"additionalProperties": false
}
}
};
7 changes: 7 additions & 0 deletions packages/siop-oid4vp/lib/types/SIOP.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,12 @@ export interface VerifiedIDToken {
verifyOpts: VerifyAuthorizationResponseOpts
}

export interface VerifiedOpenID4VPSubmissionDcql {
dcqlQuery: DcqlQuery
presentations: WrappedVerifiablePresentation[]
nonce?: string
}

export interface VerifiedOpenID4VPSubmission {
submissionData: PresentationSubmission
presentationDefinitions: PresentationDefinitionWithLocation[]
Expand All @@ -529,6 +535,7 @@ export interface VerifiedAuthorizationResponse {
authorizationResponse: AuthorizationResponse

oid4vpSubmission?: VerifiedOpenID4VPSubmission
oid4vpSubmissionDcql?: VerifiedOpenID4VPSubmissionDcql

nonce?: string
state: string
Expand Down

0 comments on commit dc1c318

Please sign in to comment.