Skip to content

Commit

Permalink
updates to PEX flow
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Jan 8, 2024
1 parent f3e1534 commit 6757add
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 144 deletions.
5 changes: 3 additions & 2 deletions packages/core/src/decorators/attachment/Attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Expose, Type } from 'class-transformer'
import { IsDate, IsHash, IsInstance, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator'

import { AriesFrameworkError } from '../../error'
import { JsonValue } from '../../types'
import { JsonEncoder } from '../../utils/JsonEncoder'
import { uuid } from '../../utils/uuid'

Expand All @@ -19,7 +20,7 @@ export interface AttachmentOptions {

export interface AttachmentDataOptions {
base64?: string
json?: Record<string, unknown>
json?: JsonValue
links?: string[]
jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat
sha256?: string
Expand All @@ -40,7 +41,7 @@ export class AttachmentData {
* Directly embedded JSON data, when representing content inline instead of via links, and when the content is natively conveyable as JSON. Optional.
*/
@IsOptional()
public json?: Record<string, unknown>
public json?: JsonValue

/**
* A list of zero or more locations at which the content may be fetched. Optional.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InputDescriptorToCredentials, PresentationSubmission } from './models'
import type { InputDescriptorToCredentials, PexCredentialsForRequest } from './models'
import type { AgentContext } from '../../agent'
import type { Query } from '../../storage/StorageService'
import type { VerificationMethod } from '../dids'
Expand All @@ -11,15 +11,21 @@ import type {
} from '@sphereon/pex'
import type {
InputDescriptorV2,
PresentationSubmission as PePresentationSubmission,
PresentationSubmission,
PresentationDefinitionV1,
PresentationDefinitionV2,
} from '@sphereon/pex-models'
import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types'
import type {
IVerifiablePresentation,
OriginalVerifiableCredential,
OriginalVerifiablePresentation,
} from '@sphereon/ssi-types'

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

import { getJwkFromKey } from '../../crypto'
import { AriesFrameworkError } from '../../error'
import { JsonTransformer } from '../../utils'
import { DidsApi, getKeyFromVerificationMethod } from '../dids'
import {
Expand All @@ -32,32 +38,61 @@ import {

import { PresentationExchangeError } from './PresentationExchangeError'
import {
selectCredentialsForRequest,
getCredentialsForRequest,
getSphereonOriginalVerifiableCredential,
getSphereonW3cVerifiablePresentation,
getW3cVerifiablePresentationInstance,
} from './utils'

// FIXME: Why are these Record<string, unknown> types used?
export type ProofStructure = Record<string, Record<string, Array<W3cVerifiableCredential>>>
export type PresentationDefinition = IPresentationDefinition & Record<string, unknown>
export type VerifiablePresentation = IVerifiablePresentation & Record<string, unknown>
export type PexPresentationSubmission = PePresentationSubmission & Record<string, unknown>
export type PexPresentationSubmission = PresentationSubmission

@injectable()
export class PresentationExchangeService {
private pex = new PEX()

public async selectCredentialsForRequest(
public async getCredentialsForRequest(
agentContext: AgentContext,
presentationDefinition: PresentationDefinition
): Promise<PresentationSubmission> {
): Promise<PexCredentialsForRequest> {
const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition)

// FIXME: why are we resolving all created dids here?
// If we want to do this we should extract all dids from the credential records and only
// fetch the dids for the queried credential records
const didsApi = agentContext.dependencyManager.resolve(DidsApi)
const didRecords = await didsApi.getCreatedDids()
const holderDids = didRecords.map((didRecord) => didRecord.did)

return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids)
return getCredentialsForRequest(presentationDefinition, credentialRecords, holderDids)
}

/**
* Selects the credentials to use based on the output from `getCredentialsForRequest`
* Use this method if you don't want to manually select the credentials yourself.
*/
public selectCredentialsForRequest(credentialsForRequest: PexCredentialsForRequest): InputDescriptorToCredentials {
if (!credentialsForRequest.areRequirementsSatisfied) {
throw new AriesFrameworkError('Could not find the required credentials for the presentation submission')
}

const credentials: InputDescriptorToCredentials = {}

for (const requirement of credentialsForRequest.requirements) {
for (const submission of requirement.submissionEntry) {
if (!credentials[submission.inputDescriptorId]) {
credentials[submission.inputDescriptorId] = []
}

// We pick the first matching VC if we are auto-selecting
credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0].credential)
}
}

return credentials
}

public validatePresentationDefinition(presentationDefinition: PresentationDefinition) {
Expand All @@ -76,8 +111,11 @@ export class PresentationExchangeService {
}
}

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

if (errors) {
const errorMessages = this.formatValidated(errors as Validated)
Expand Down Expand Up @@ -201,12 +239,16 @@ export class PresentationExchangeService {
options: {
credentialsForInputDescriptor: InputDescriptorToCredentials
presentationDefinition: PresentationDefinition
/**
* Defaults to {@link PresentationSubmissionLocation.PRESENTATION}
*/
presentationSubmissionLocation?: PresentationSubmissionLocation
challenge?: string
domain?: string
nonce?: string
}
) {
const { presentationDefinition, challenge, nonce, domain } = options
const { presentationDefinition, challenge, nonce, domain, presentationSubmissionLocation } = options

const proofStructure: ProofStructure = {}

Expand Down Expand Up @@ -267,7 +309,7 @@ export class PresentationExchangeService {
holderDID: subjectId,
proofOptions: { challenge, domain, nonce },
signatureOptions: { verificationMethod: verificationMethod?.id },
presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
presentationSubmissionLocation: presentationSubmissionLocation ?? PresentationSubmissionLocation.PRESENTATION,
}
)

Expand Down Expand Up @@ -371,7 +413,7 @@ export class PresentationExchangeService {
}

private getSigningAlgorithmForJwtVc(
presentationDefinition: PresentationDefinition,
presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2,
verificationMethod: VerificationMethod
) {
const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? []
Expand All @@ -390,7 +432,7 @@ export class PresentationExchangeService {

private getProofTypeForLdpVc(
agentContext: AgentContext,
presentationDefinition: PresentationDefinition,
presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2,
verificationMethod: VerificationMethod
) {
const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? []
Expand Down Expand Up @@ -451,31 +493,23 @@ export class PresentationExchangeService {
)
}

// Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation.
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 as PresentationDefinition, verificationMethod),
alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod),
verificationMethod: verificationMethod.id,
presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation),
presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation),
challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()),
domain,
})
} else if (vpFormat === 'ldp_vp') {
signedPresentation = await w3cCredentialService.signPresentation(agentContext, {
format: ClaimFormat.LdpVp,
proofType: this.getProofTypeForLdpVc(
agentContext,
presentationDefinition as PresentationDefinition,
verificationMethod
),
proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod),
proofPurpose: 'authentication',
verificationMethod: verificationMethod.id,
presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation),
presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation),
challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()),
domain,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc'

export interface PresentationSubmission {
export interface PexCredentialsForRequest {
/**
* Whether all requirements have been satisfied by the credentials in the wallet.
*/
Expand All @@ -16,7 +16,7 @@ export interface PresentationSubmission {
* combinations that are possible. The structure doesn't include all possible combinations yet that
* could satisfy a presentation definition.
*/
requirements: PresentationSubmissionRequirement[]
requirements: PexCredentialsForRequestRequirement[]

/**
* Name of the presentation definition
Expand All @@ -36,7 +36,7 @@ export interface PresentationSubmission {
*
* Each submission represents a input descriptor.
*/
export interface PresentationSubmissionRequirement {
export interface PexCredentialsForRequestRequirement {
/**
* Whether the requirement is satisfied.
*
Expand All @@ -56,7 +56,7 @@ export interface PresentationSubmissionRequirement {
purpose?: string

/**
* Array of objects, where each entry contains a credential that will be part
* Array of objects, where each entry contains one or more credentials that will be part
* of the submission.
*
* NOTE: if the `isRequirementSatisfied` is `false` the submission list will
Expand All @@ -66,7 +66,7 @@ export interface PresentationSubmissionRequirement {
* `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value
* to see how many of those submissions needed.
*/
submissionEntry: SubmissionEntry[]
submissionEntry: PexCredentialsForRequestSubmissionEntry[]

/**
* The number of submission entries that are needed to fulfill the requirement.
Expand All @@ -88,7 +88,7 @@ export interface PresentationSubmissionRequirement {
* A submission entry that satisfies a specific input descriptor from the
* presentation definition.
*/
export interface SubmissionEntry {
export interface PexCredentialsForRequestSubmissionEntry {
/**
* The id of the input descriptor
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { W3cCredentialRecord } from '../../vc'
import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from '../models'
import type {
PexCredentialsForRequest,
PexCredentialsForRequestRequirement,
PexCredentialsForRequestSubmissionEntry,
} from '../models'
import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex'
import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models'

Expand All @@ -12,11 +16,11 @@ import { PresentationExchangeError } from '../PresentationExchangeError'

import { getSphereonOriginalVerifiableCredential } from './transform'

export async function selectCredentialsForRequest(
export async function getCredentialsForRequest(
presentationDefinition: IPresentationDefinition,
credentialRecords: Array<W3cCredentialRecord>,
holderDIDs: Array<string>
): Promise<PresentationSubmission> {
): Promise<PexCredentialsForRequest> {
if (!presentationDefinition) {
throw new PresentationExchangeError('Presentation Definition is required to select credentials for submission.')
}
Expand Down Expand Up @@ -50,7 +54,7 @@ export async function selectCredentialsForRequest(
}),
}

const presentationSubmission: PresentationSubmission = {
const presentationSubmission: PexCredentialsForRequest = {
requirements: [],
areRequirementsSatisfied: false,
name: presentationDefinition.name,
Expand Down Expand Up @@ -92,8 +96,8 @@ export async function selectCredentialsForRequest(
function getSubmissionRequirements(
presentationDefinition: IPresentationDefinition,
selectResults: W3cCredentialRecordSelectResults
): Array<PresentationSubmissionRequirement> {
const submissionRequirements: Array<PresentationSubmissionRequirement> = []
): Array<PexCredentialsForRequestRequirement> {
const submissionRequirements: Array<PexCredentialsForRequestRequirement> = []

// There are submission requirements, so we need to select the input_descriptors
// based on the submission requirements
Expand Down Expand Up @@ -138,8 +142,8 @@ function getSubmissionRequirements(
function getSubmissionRequirementsForAllInputDescriptors(
inputDescriptors: Array<InputDescriptorV1> | Array<InputDescriptorV2>,
selectResults: W3cCredentialRecordSelectResults
): Array<PresentationSubmissionRequirement> {
const submissionRequirements: Array<PresentationSubmissionRequirement> = []
): Array<PexCredentialsForRequestRequirement> {
const submissionRequirements: Array<PexCredentialsForRequestRequirement> = []

for (const inputDescriptor of inputDescriptors) {
const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults)
Expand All @@ -164,7 +168,7 @@ function getSubmissionRequirementRuleAll(
if (!submissionRequirement.from)
throw new PresentationExchangeError("Missing 'from' in submission requirement match.")

const selectedSubmission: PresentationSubmissionRequirement = {
const selectedSubmission: PexCredentialsForRequestRequirement = {
rule: Rules.All,
needsCount: 0,
name: submissionRequirement.name,
Expand Down Expand Up @@ -204,7 +208,7 @@ function getSubmissionRequirementRulePick(
throw new PresentationExchangeError("Missing 'from' in submission requirement match.")
}

const selectedSubmission: PresentationSubmissionRequirement = {
const selectedSubmission: PexCredentialsForRequestRequirement = {
rule: Rules.Pick,
needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1,
name: submissionRequirement.name,
Expand All @@ -215,8 +219,8 @@ function getSubmissionRequirementRulePick(
isRequirementSatisfied: false,
}

const satisfiedSubmissions: Array<SubmissionEntry> = []
const unsatisfiedSubmissions: Array<SubmissionEntry> = []
const satisfiedSubmissions: Array<PexCredentialsForRequestSubmissionEntry> = []
const unsatisfiedSubmissions: Array<PexCredentialsForRequestSubmissionEntry> = []

for (const inputDescriptor of presentationDefinition.input_descriptors) {
// We only want to get the submission if the input descriptor belongs to the group
Expand Down Expand Up @@ -254,7 +258,7 @@ function getSubmissionRequirementRulePick(
function getSubmissionForInputDescriptor(
inputDescriptor: InputDescriptorV1 | InputDescriptorV2,
selectResults: W3cCredentialRecordSelectResults
): SubmissionEntry {
): PexCredentialsForRequestSubmissionEntry {
// https://github.com/Sphereon-Opensource/PEX/issues/116
// If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it
const matchesForInputDescriptor = selectResults.matches?.filter(
Expand All @@ -264,7 +268,7 @@ function getSubmissionForInputDescriptor(
m.name === inputDescriptor.name
)

const submissionEntry: SubmissionEntry = {
const submissionEntry: PexCredentialsForRequestSubmissionEntry = {
inputDescriptorId: inputDescriptor.id,
name: inputDescriptor.name,
purpose: inputDescriptor.purpose,
Expand Down
Loading

0 comments on commit 6757add

Please sign in to comment.