Skip to content

Commit

Permalink
feat(present-proof): working initial full flow
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <sliedrecht@berend.io>
  • Loading branch information
Berend Sliedrecht committed Dec 19, 2023
1 parent c170c98 commit 3fbf006
Show file tree
Hide file tree
Showing 11 changed files with 611 additions and 583 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresent
import type {
IPresentationDefinition,
PresentationSignCallBackParams,
Validated,
VerifiablePresentationResult,
} from '@sphereon/pex'
import type {
InputDescriptorV2,
PresentationSubmission as PexPresentationSubmission,
PresentationDefinitionV1,
} from '@sphereon/pex-models'
import type { OriginalVerifiableCredential } from '@sphereon/ssi-types'
import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types'

import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex'
import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex'
import { injectable } from 'tsyringe'

import { getJwkFromKey } from '../../crypto'
Expand All @@ -38,7 +39,9 @@ import {
} from './utils'

export type ProofStructure = Record<string, Record<string, Array<W3cVerifiableCredential>>>
export type PresentationDefinition = IPresentationDefinition
export type PresentationDefinition = IPresentationDefinition & Record<string, unknown>

export type VerifiablePresentation = IVerifiablePresentation & Record<string, unknown>

@injectable()
export class PresentationExchangeService {
Expand All @@ -57,6 +60,50 @@ export class PresentationExchangeService {
return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids)
}

public validatePresentationDefinition(presentationDefinition: PresentationDefinition) {
const validation = PEX.validateDefinition(presentationDefinition)
const errorMessages = this.formatValidated(validation)
if (errorMessages.length > 0) {
throw new PresentationExchangeError(
`Invalid presentation definition. The following errors were found: ${errorMessages.join(', ')}`
)
}
}

public validatePresentationSubmission(presentationSubmission: PexPresentationSubmission) {
const validation = PEX.validateSubmission(presentationSubmission)
const errorMessages = this.formatValidated(validation)
if (errorMessages.length > 0) {
throw new PresentationExchangeError(
`Invalid presentation submission. The following errors were found: ${errorMessages.join(', ')}`
)
}
}

public validatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentation) {
const { errors } = this.pex.evaluatePresentation(presentationDefinition, presentation)

if (errors) {
const errorMessages = this.formatValidated(errors as Validated)
if (errorMessages.length > 0) {
throw new PresentationExchangeError(
`Invalid presentation. The following errors were found: ${errorMessages.join(', ')}`
)
}
}
}

private formatValidated(v: Validated) {
return Array.isArray(v)
? (v
.filter((r) => r.tag === Status.ERROR)
.map((r) => r.message)
.filter((m) => Boolean(m)) as Array<string>)
: v.tag === Status.ERROR && typeof v.message === 'string'
? [v.message]
: []
}

/**
* Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the
* schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors.
Expand Down Expand Up @@ -100,9 +147,12 @@ export class PresentationExchangeService {

// query the wallet ourselves first to avoid the need to query the pex library for all
// credentials for every proof request
const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, {
$or: query,
})
const credentialRecords =
query.length > 0
? await w3cCredentialRepository.findByQuery(agentContext, {
$or: query,
})
: await w3cCredentialRepository.getAll(agentContext)

return credentialRecords
}
Expand Down Expand Up @@ -237,15 +287,10 @@ export class PresentationExchangeService {
throw new PresentationExchangeError('No verifiable presentations created.')
}

if (!verifiablePresentationResultsWithFormat[0]) {
throw new PresentationExchangeError('No verifiable presentations created.')
}

if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) {
throw new PresentationExchangeError('Invalid amount of verifiable presentations created.')
}

verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission
const presentationSubmission: PexPresentationSubmission = {
id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id,
definition_id:
Expand Down Expand Up @@ -416,13 +461,14 @@ export class PresentationExchangeService {
}

// Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation.
const presentationToSign = { ...presentationJson, presentation_submission: undefined }
const presentationToSign = { ...presentationJson }
delete presentationToSign['presentation_submission']

let signedPresentation: W3cVerifiablePresentation<ClaimFormat.JwtVp | ClaimFormat.LdpVp>
if (vpFormat === 'jwt_vp') {
signedPresentation = await w3cCredentialService.signPresentation(agentContext, {
format: ClaimFormat.JwtVp,
alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod),
alg: this.getSigningAlgorithmForJwtVc(presentationDefinition as PresentationDefinition, verificationMethod),
verificationMethod: verificationMethod.id,
presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation),
challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()),
Expand All @@ -431,7 +477,11 @@ export class PresentationExchangeService {
} else if (vpFormat === 'ldp_vp') {
signedPresentation = await w3cCredentialService.signPresentation(agentContext, {
format: ClaimFormat.LdpVp,
proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod),
proofType: this.getProofTypeForLdpVc(
agentContext,
presentationDefinition as PresentationDefinition,
verificationMethod
),
proofPurpose: 'authentication',
verificationMethod: verificationMethod.id,
presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PEX } from '@sphereon/pex'
import { Rules } from '@sphereon/pex-models'
import { default as jp } from 'jsonpath'

import { deepEquality } from '../../../utils'
import { PresentationExchangeError } from '../PresentationExchangeError'

import { getSphereonOriginalVerifiableCredential } from './transform'
Expand All @@ -16,14 +17,14 @@ export async function selectCredentialsForRequest(
credentialRecords: Array<W3cCredentialRecord>,
holderDIDs: Array<string>
): Promise<PresentationSubmission> {
const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential))

if (!presentationDefinition) {
throw new PresentationExchangeError('Presentation Definition is required to select credentials for submission.')
}

const pex = new PEX()

const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential))

// FIXME: there is a function for this in the VP library, but it is not usable atm
const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, {
holderDIDs,
Expand All @@ -36,8 +37,11 @@ export async function selectCredentialsForRequest(
...selectResultsRaw,
// Map the encoded credential to their respective w3c credential record
verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => {
const credentialIndex = encodedCredentials.indexOf(encoded)
const credentialRecord = credentialRecords[credentialIndex]
const credentialRecord = credentialRecords.find((record) => {
const originalVc = getSphereonOriginalVerifiableCredential(record.credential)
return deepEquality(originalVc, encoded)
})

if (!credentialRecord) {
throw new PresentationExchangeError('Unable to find credential in credential records.')
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../../vc'
import type { PresentationDefinition } from '../../services'
import type { PresentationDefinition } from '../../../presentation-exchange'
import type { W3cCredentialRecord } from '../../../vc'
import type { ProofFormat } from '../ProofFormat'

export interface PresentationExchangeProofFormat extends ProofFormat {
Expand All @@ -20,7 +20,7 @@ export interface PresentationExchangeProofFormat extends ProofFormat {
}

acceptRequest: {
credentials: Record<string, Array<W3cVerifiableCredential>>
credentials: Array<W3cCredentialRecord>
}

getCredentialsForRequest: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { PresentationExchangeProofFormat } from './PresentationExchangeProofFormat'
import type { AgentContext } from '../../../../agent'
import type { W3cCredentialRecord } from '../../../vc'
import type {
PresentationDefinition,
VerifiablePresentation,
} from '../../../presentation-exchange/PresentationExchangeService'
import type { W3cCredentialRecord, W3cVerifiablePresentation } from '../../../vc'
import type { InputDescriptorToCredentials } from '../../models'
import type { PresentationDefinition, VerifiablePresentation } from '../../services'
import type { ProofFormatService } from '../ProofFormatService'
import type {
ProofFormatCreateProposalOptions,
Expand All @@ -18,13 +21,13 @@ import type {
ProofFormatAutoRespondRequestOptions,
ProofFormatAutoRespondPresentationOptions,
} from '../ProofFormatServiceOptions'
import type { PresentationSubmission as PexPresentationSubmission } from '@sphereon/pex-models'
import type { PresentationSubmission as PexPresentationSubmission, PresentationSubmission } from '@sphereon/pex-models'

import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment'
import { AriesFrameworkError } from '../../../../error'
import { deepEquality } from '../../../../utils'
import { PresentationExchangeService } from '../../../presentation-exchange/PresentationExchangeService'
import { ProofFormatSpec } from '../../models'
import { PresentationExchangeService } from '../../services'

const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0'
const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0'
Expand Down Expand Up @@ -94,7 +97,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic

ps.validatePresentationDefinition(presentationDefinition)

const attachment = this.getFormatData(presentationDefinition, format.attachmentId)
const attachment = this.getFormatData({ presentation_definition: presentationDefinition }, format.attachmentId)

return { format, attachment }
}
Expand All @@ -120,15 +123,22 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic
attachmentId,
})

const attachment = this.getFormatData(presentationDefinition, format.attachmentId)
const options = { challenge: 'TODO' }

const attachment = this.getFormatData(
{ options, presentation_definition: presentationDefinition },
format.attachmentId
)

return { attachment, format }
}

public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise<void> {
const ps = this.presentationExchangeService(agentContext)
const proposal = attachment.getDataAsJson<PresentationDefinition>()
ps.validatePresentationDefinition(proposal)
const { presentation_definition: presentationDefinition } = attachment.getDataAsJson<{
presentation_definition: PresentationDefinition
}>()
ps.validatePresentationDefinition(presentationDefinition)
}

public async acceptRequest(
Expand All @@ -148,7 +158,10 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic
attachmentId,
})

const presentationDefinition = requestAttachment.getDataAsJson<PresentationDefinition>()
const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{
presentation_definition: PresentationDefinition
options?: { challenge?: string; domain?: string }
}>()

const { areRequirementsSatisfied, requirements } = await ps.selectCredentialsForRequest(
agentContext,
Expand All @@ -172,7 +185,16 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic
credentialsForInputDescriptor: credentials,
})

const attachment = this.getFormatData(presentation, format.attachmentId)
if (presentation.verifiablePresentations.length > 1) {
throw new AriesFrameworkError('Invalid amount of verifiable presentations. Only one is allowed.')
}

const data = {
presentation_submission: presentation.presentationSubmission,
...presentation.verifiablePresentations[0],
}

const attachment = this.getFormatData(data, format.attachmentId)

return { attachment, format }
}
Expand All @@ -182,16 +204,20 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic
{ requestAttachment, attachment }: ProofFormatProcessPresentationOptions
): Promise<boolean> {
const ps = this.presentationExchangeService(agentContext)
const presentationDefinition = requestAttachment.getDataAsJson<PresentationDefinition>()
const presentation = attachment.getDataAsJson<VerifiablePresentation>()
const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{
presentation_definition: PresentationDefinition
}>()
const presentation = attachment.getDataAsJson<
W3cVerifiablePresentation & { presentation_submission: PresentationSubmission }
>()

try {
ps.validatePresentationDefinition(presentationDefinition)
if (presentation.presentation_submission) {
ps.validatePresentationSubmission(presentation.presentation_submission as unknown as PexPresentationSubmission)
ps.validatePresentationSubmission(presentation.presentation_submission)
}

ps.validatePresentation(presentationDefinition, presentation)
ps.validatePresentation(presentationDefinition, presentation as unknown as VerifiablePresentation)
return true
} catch (e) {
agentContext.config.logger.error(e)
Expand All @@ -204,7 +230,9 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic
{ requestAttachment }: ProofFormatGetCredentialsForRequestOptions<PresentationExchangeProofFormat>
): Promise<Array<W3cCredentialRecord>> {
const ps = this.presentationExchangeService(agentContext)
const presentationDefinition = requestAttachment.getDataAsJson<PresentationDefinition>()
const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{
presentation_definition: PresentationDefinition
}>()

ps.validatePresentationDefinition(presentationDefinition)

Expand All @@ -222,7 +250,9 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic
{ requestAttachment }: ProofFormatSelectCredentialsForRequestOptions<PresentationExchangeProofFormat>
): Promise<Array<W3cCredentialRecord>> {
const ps = this.presentationExchangeService(agentContext)
const presentationDefinition = requestAttachment.getDataAsJson<PresentationDefinition>()
const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{
presentation_definition: PresentationDefinition
}>()

ps.validatePresentationDefinition(presentationDefinition)

Expand Down
Loading

0 comments on commit 3fbf006

Please sign in to comment.