Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dcql' into feature/SPRIND-137
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/siop-oid4vp/package.json
#	pnpm-lock.yaml
  • Loading branch information
zoemaas committed Jan 8, 2025
2 parents fd6a07b + ad19797 commit 9e2035b
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 78 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@
"OIDC4VP",
"OID4VCI",
"OID4VP"
]
],
"packageManager": "pnpm@9.6.0+sha256.dae0f7e822c56b20979bb5965e3b73b8bdabb6b8b8ef121da6d857508599ca35"
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class AuthorizationRequest {
return await PresentationExchange.findValidPresentationDefinitions(await this.mergedPayloads(), version)
}

public async getDcqlQuery(): Promise<DcqlQuery> {
public async getDcqlQuery(): Promise<DcqlQuery | undefined> {
return await findValidDcqlQuery(await this.mergedPayloads())
}
}
4 changes: 2 additions & 2 deletions packages/siop-oid4vp/lib/authorization-request/Payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ export const createPresentationDefinitionClaimsProperties = (opts: ClaimPayloadO

return {
...(opts.id_token ? { id_token: opts.id_token } : {}),
...((opts.vp_token.presentation_definition || opts.vp_token.presentation_definition_uri || opts.vp_token.dcql_query) && {
...((opts.vp_token.presentation_definition || opts.vp_token.presentation_definition_uri) && {
vp_token: {
...(!opts.vp_token.presentation_definition_uri && { presentation_definition: opts.vp_token.presentation_definition }),
...(opts.vp_token.presentation_definition_uri && { presentation_definition_uri: opts.vp_token.presentation_definition_uri }),
...(opts.vp_token.dcql_query && { dcql_query: opts.vp_token.dcql_query }),
},
}),
...(opts.vp_token.dcql_query && { vp_token: { dcql_query: opts.vp_token.dcql_query } }),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { CredentialMapper, Hasher, WrappedVerifiablePresentation } from '@sphereon/ssi-types'
import { DcqlPresentationRecord } from 'dcql'
import { DcqlPresentation } 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 { assertValidDcqlPresentationRecord } from './Dcql'
import { assertValidDcqlPresentationResult } from './Dcql'
import {
assertValidVerifiablePresentations,
extractNonceFromWrappedVerifiablePresentation,
Expand Down Expand Up @@ -126,32 +126,30 @@ export class AuthorizationResponse {
authorizationRequest,
})

if (hasVpToken) {
if (responseOpts.presentationExchange) {
const wrappedPresentations = response.payload.vp_token
? await extractPresentationsFromVpToken(response.payload.vp_token, {
hasher: verifyOpts.hasher,
})
: []

await assertValidVerifiablePresentations({
presentationDefinitions,
presentations: wrappedPresentations,
verificationCallback: verifyOpts.verification.presentationVerificationCallback,
opts: {
...responseOpts.presentationExchange,
if (!hasVpToken) return response

if (responseOpts.presentationExchange) {
const wrappedPresentations = response.payload.vp_token
? extractPresentationsFromVpToken(response.payload.vp_token, {
hasher: verifyOpts.hasher,
},
})
} else {
const dcqlQuery = verifiedAuthorizationRequest.dcqlQuery
if (!dcqlQuery) {
throw new Error('vp_token is present, but no presentation definitions or dcql query provided')
}
assertValidDcqlPresentationRecord(responseOpts.dcqlQuery.encodedPresentationRecord as DcqlPresentationRecord, dcqlQuery, {
})
: []

await assertValidVerifiablePresentations({
presentationDefinitions,
presentations: wrappedPresentations,
verificationCallback: verifyOpts.verification.presentationVerificationCallback,
opts: {
...responseOpts.presentationExchange,
hasher: verifyOpts.hasher,
})
}
},
})
} else if (verifiedAuthorizationRequest.dcqlQuery) {
assertValidDcqlPresentationResult(responseOpts.dcqlQuery.dcqlPresentation as DcqlPresentation, verifiedAuthorizationRequest.dcqlQuery, {
hasher: verifyOpts.hasher,
})
} else {
throw new Error('vp_token is present, but no presentation definitions or dcql query provided')
}

return response
Expand All @@ -169,19 +167,19 @@ export class AuthorizationResponse {

const verifiedIdToken = await this.idToken?.verify(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'))
throw new 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'))
throw new 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>()
if (oid4vp && oid4vp.nonce) allNonces.add(oid4vp.nonce)
if (oid4vp && (oid4vp.dcql?.nonce || oid4vp.presentationExchange?.nonce)) allNonces.add(oid4vp.dcql?.nonce ?? oid4vp.presentationExchange?.nonce)
if (verifiedIdToken) allNonces.add(verifiedIdToken.payload.nonce)
if (merged.nonce) allNonces.add(merged.nonce)

Expand All @@ -207,8 +205,8 @@ export class AuthorizationResponse {
state,
correlationId: verifyOpts.correlationId,
...(this.idToken && { idToken: verifiedIdToken }),
...(oid4vp && 'presentationDefinitions' in oid4vp && { oid4vpSubmission: oid4vp }),
...(oid4vp && 'dcqlQuery' in oid4vp && { oid4vpSubmissionDcql: oid4vp }),
...(oid4vp?.presentationExchange && { oid4vpSubmission: oid4vp.presentationExchange }),
...(oid4vp?.dcql && { oid4vpSubmissionDcql: oid4vp.dcql }),
}
}

Expand Down
39 changes: 21 additions & 18 deletions packages/siop-oid4vp/lib/authorization-response/Dcql.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Hasher } from '@sphereon/ssi-types'
import { DcqlMdocRepresentation, DcqlPresentationRecord, DcqlQuery, DcqlSdJwtVcRepresentation } from 'dcql'
import { DcqlPresentationQueryResult } from 'dcql'
import { DcqlMdocCredential, DcqlPresentation, DcqlPresentationResult, DcqlQuery, DcqlSdJwtVcCredential } from 'dcql'

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

import { extractPresentationRecordFromDcqlVpToken } from './OpenID4VP'
import { extractDcqlPresentationFromDcqlVpToken } from './OpenID4VP'

/**
* Finds a valid DcqlQuery inside the given AuthenticationRequestPayload
Expand Down Expand Up @@ -38,22 +37,26 @@ export const findValidDcqlQuery = async (authorizationRequestPayload: Authorizat
return DcqlQuery.parse(JSON.parse(dcqlQuery[0]))
}

export const getDcqlPresentationResult = (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')
}
})

return DcqlPresentationQueryResult.query(credentials, { dcqlQuery })
export const getDcqlPresentationResult = (record: DcqlPresentation | string, dcqlQuery: DcqlQuery, opts: { hasher?: Hasher }) => {
const dcqlPresentation = Object.fromEntries(
Object.entries(extractDcqlPresentationFromDcqlVpToken(record, opts)).map(([queryId, p]) => {
if (p.format === 'mso_mdoc') {
return [
queryId,
{ credential_format: 'mso_mdoc', doctype: p.vcs[0].credential.toJson().docType, namespaces: p.vcs[0].decoded } satisfies DcqlMdocCredential,
]
} else if (p.format === 'vc+sd-jwt') {
return [queryId, { credential_format: 'vc+sd-jwt', vct: p.vcs[0].decoded.vct, claims: p.vcs[0].decoded } satisfies DcqlSdJwtVcCredential]
} else {
throw new Error('DcqlPresentation atm only supports mso_mdoc and vc+sd-jwt')
}
}),
)

return DcqlPresentationResult.fromDcqlPresentation(dcqlPresentation, { dcqlQuery })
}

export const assertValidDcqlPresentationRecord = async (record: DcqlPresentationRecord | string, dcqlQuery: DcqlQuery, opts: { hasher?: Hasher }) => {
export const assertValidDcqlPresentationResult = async (record: DcqlPresentation | string, dcqlQuery: DcqlQuery, opts: { hasher?: Hasher }) => {
const result = getDcqlPresentationResult(record, dcqlQuery, opts)
return DcqlPresentationQueryResult.validate(result)
return DcqlPresentationResult.validate(result)
}
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 @@ -9,7 +9,7 @@ import {
W3CVerifiablePresentation,
WrappedVerifiablePresentation,
} from '@sphereon/ssi-types'
import { DcqlPresentationRecord, DcqlQuery } from 'dcql'
import { DcqlPresentation, DcqlQuery } from 'dcql'

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

import { AuthorizationResponse } from './AuthorizationResponse'
import { assertValidDcqlPresentationRecord } from './Dcql'
import { assertValidDcqlPresentationResult } from './Dcql'
import { PresentationExchange } from './PresentationExchange'
import {
AuthorizationResponseOpts,
Expand Down Expand Up @@ -67,7 +67,7 @@ export const extractNonceFromWrappedVerifiablePresentation = (wrappedVp: Wrapped
export const verifyPresentations = async (
authorizationResponse: AuthorizationResponse,
verifyOpts: VerifyAuthorizationResponseOpts,
): Promise<VerifiedOpenID4VPSubmission | VerifiedOpenID4VPSubmissionDcql | null> => {
): Promise<{ presentationExchange?: VerifiedOpenID4VPSubmission; dcql?: VerifiedOpenID4VPSubmissionDcql }> => {
let idPayload: IDTokenPayload | undefined
if (authorizationResponse.idToken) {
idPayload = await authorizationResponse.idToken.payload()
Expand All @@ -82,18 +82,21 @@ export const verifyPresentations = async (

let presentationSubmission: PresentationSubmission | undefined

let dcqlPresentation: { [credentialQueryId: string]: WrappedVerifiablePresentation } | undefined

let dcqlQuery = verifyOpts.dcqlQuery ?? authorizationResponse?.authorizationRequest?.payload.dcql_query
if (dcqlQuery) {
dcqlQuery = DcqlQuery.parse(dcqlQuery)
wrappedPresentations = extractPresentationsFromDcqlVpToken(authorizationResponse.payload.vp_token as string, { hasher: verifyOpts.hasher })
dcqlPresentation = extractDcqlPresentationFromDcqlVpToken(authorizationResponse.payload.vp_token as string, { hasher: verifyOpts.hasher })
wrappedPresentations = Object.values(dcqlPresentation)

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

assertValidDcqlPresentationRecord(authorizationResponse.payload.vp_token as string, dcqlQuery, { hasher: verifyOpts.hasher })
assertValidDcqlPresentationResult(authorizationResponse.payload.vp_token as string, dcqlQuery, { hasher: verifyOpts.hasher })

if (verifiedPresentations.some((verified) => !verified)) {
const message = verifiedPresentations
Expand All @@ -105,7 +108,7 @@ export const verifyPresentations = async (
}
} else {
const presentations = authorizationResponse.payload.vp_token
? await extractPresentationsFromVpToken(authorizationResponse.payload.vp_token, { hasher: verifyOpts.hasher })
? extractPresentationsFromVpToken(authorizationResponse.payload.vp_token, { hasher: verifyOpts.hasher })
: []
wrappedPresentations = Array.isArray(presentations) ? presentations : [presentations]

Expand Down Expand Up @@ -149,31 +152,31 @@ export const verifyPresentations = async (
}
}
if (presentationDefinitions) {
return { nonce, presentations: wrappedPresentations, presentationDefinitions, submissionData: presentationSubmission }
return { presentationExchange: { nonce, presentations: wrappedPresentations, presentationDefinitions, submissionData: presentationSubmission } }
} else {
return { nonce, presentations: wrappedPresentations, dcqlQuery }
return { dcql: { nonce, presentation: dcqlPresentation, dcqlQuery } }
}
}

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

return presentationRecord
return dcqlPresentation
}

export const extractPresentationsFromDcqlVpToken = (
vpToken: DcqlPresentationRecord.Input | string,
vpToken: DcqlPresentation.Input | string,
opts?: { hasher?: Hasher },
): WrappedVerifiablePresentation[] => {
return Object.values(extractPresentationRecordFromDcqlVpToken(vpToken, opts))
return Object.values(extractDcqlPresentationFromDcqlVpToken(vpToken, opts))
}

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

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

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

export interface DcqlQueryResponseOpts {
encodedPresentationRecord: Record<string, Record<string, unknown> | string>
dcqlPresentation: 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": {
"encodedPresentationRecord": {
"dcqlPresentation": {
"type": "object",
"additionalProperties": {
"anyOf": [
Expand All @@ -2358,7 +2358,7 @@ export const AuthorizationResponseOptsSchemaObj = {
}
},
"required": [
"encodedPresentationRecord"
"dcqlPresentation"
],
"additionalProperties": false
}
Expand Down
2 changes: 1 addition & 1 deletion packages/siop-oid4vp/lib/types/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ enum SIOPErrors {
REFERENCE_URI_NO_PAYLOAD = 'referenceUri specified, but object to host there is not present',
NO_SELF_ISSUED_ISS = 'The Response Token Issuer Claim (iss) MUST start with https://self-isued.me/v2',
REGISTRATION_NOT_SET = 'Registration metadata not set.',
REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE = "Request claims can't multiple of 'presentation_definition' and 'presentation_definition_uri' 'dcql_query",
REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE = "Request claims can't have multiple of 'presentation_definition', 'presentation_definition_uri' and 'dcql_query",
REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID = 'Presentation definition in the request claims is not valid',
REQUEST_OBJECT_TYPE_NOT_SET = 'Request object type is not set.',
RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID = 'presentation_submission object inside the response opts vp should be valid',
Expand Down
6 changes: 3 additions & 3 deletions packages/siop-oid4vp/lib/types/SIOP.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export interface IDTokenPayload extends JWTPayload {
}
}

export type EcodedDcqlQueryVpToken = string
export type EncodedDcqlQueryVpToken = string

export interface AuthorizationResponsePayload {
access_token?: string
Expand All @@ -182,7 +182,7 @@ export interface AuthorizationResponsePayload {
| W3CVerifiablePresentation
| CompactSdJwtVc
| MdocOid4vpMdocVpToken
| EcodedDcqlQueryVpToken
| EncodedDcqlQueryVpToken
presentation_submission?: PresentationSubmission
verifiedData?: IPresentation | AdditionalClaims
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -519,7 +519,7 @@ export interface VerifiedIDToken {

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

Expand Down

0 comments on commit 9e2035b

Please sign in to comment.