Skip to content

Commit

Permalink
feat: method to retrieve credentials for proof request (#329)
Browse files Browse the repository at this point in the history
* Changed getRequestedCredentialsForProofRequest to return multiple choices

Signed-off-by: Patrick Kenyon <treek.kenyon@gmail.com>
  • Loading branch information
TheTreek committed Jun 22, 2021
1 parent 5c16cc1 commit 012afa6
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 86 deletions.
6 changes: 4 additions & 2 deletions src/__tests__/proofs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,11 @@ describe('Present Proof', () => {

testLogger.test('Alice accepts presentation request from Faber')
const indyProofRequest = aliceProofRecord.requestMessage?.indyProofRequest
const requestedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(
const retrievedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(
indyProofRequest!,
presentationPreview
)
const requestedCredentials = aliceAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials)
await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials)

testLogger.test('Faber waits for presentation from Alice')
Expand Down Expand Up @@ -216,10 +217,11 @@ describe('Present Proof', () => {

testLogger.test('Alice accepts presentation request from Faber')
const indyProofRequest = aliceProofRecord.requestMessage?.indyProofRequest
const requestedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(
const retrievedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(
indyProofRequest!,
presentationPreview
)
const requestedCredentials = aliceAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials)
await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials)

testLogger.test('Faber waits for presentation from Alice')
Expand Down
24 changes: 18 additions & 6 deletions src/modules/proofs/ProofsModule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PresentationPreview } from './messages'
import type { RequestedCredentials } from './models'
import type { RequestedCredentials, RetrievedCredentials } from './models'
import type { ProofRecord } from './repository/ProofRecord'

import { Lifecycle, scoped } from 'tsyringe'
Expand Down Expand Up @@ -193,24 +193,36 @@ export class ProofsModule {
}

/**
* Create a RequestedCredentials object. Given input proof request and presentation proposal,
* Create a {@link RetrievedCredentials} object. Given input proof request and presentation proposal,
* use credentials in the wallet to build indy requested credentials object for input to proof creation.
* If restrictions allow, self attested attributes will be used.
*
* Use the return value of this method as input to {@link ProofService.createPresentation} to automatically
* accept a received presentation request.
*
* @param proofRequest The proof request to build the requested credentials object from
* @param presentationProposal Optional presentation proposal to improve credential selection algorithm
* @returns Requested credentials object for use in proof creation
* @returns RetrievedCredentials object
*/
public async getRequestedCredentialsForProofRequest(
proofRequest: ProofRequest,
presentationProposal?: PresentationPreview
) {
): Promise<RetrievedCredentials> {
return this.proofService.getRequestedCredentialsForProofRequest(proofRequest, presentationProposal)
}

/**
* Takes a RetrievedCredentials object and auto selects credentials in a RequestedCredentials object
*
* Use the return value of this method as input to {@link ProofService.createPresentation} to
* automatically accept a received presentation request.
*
* @param retrievedCredentials The retrieved credentials object to get credentials from
*
* @returns RequestedCredentials
*/
public autoSelectCredentialsForProofRequest(retrievedCredentials: RetrievedCredentials): RequestedCredentials {
return this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials)
}

/**
* Retrieve all proof records
*
Expand Down
8 changes: 7 additions & 1 deletion src/modules/proofs/models/RequestedAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Expose } from 'class-transformer'
import { Exclude, Expose } from 'class-transformer'
import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator'

import { IndyCredentialInfo } from '../../credentials'

/**
* Requested Attribute for Indy proof creation
*/
Expand All @@ -10,6 +12,7 @@ export class RequestedAttribute {
this.credentialId = options.credentialId
this.timestamp = options.timestamp
this.revealed = options.revealed
this.credentialInfo = options.credentialInfo
}
}

Expand All @@ -25,4 +28,7 @@ export class RequestedAttribute {

@IsBoolean()
public revealed!: boolean

@Exclude({ toPlainOnly: true })
public credentialInfo!: IndyCredentialInfo
}
2 changes: 1 addition & 1 deletion src/modules/proofs/models/RequestedCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface RequestedCredentialsOptions {
* @see https://github.com/hyperledger/indy-sdk/blob/57dcdae74164d1c7aa06f2cccecaae121cefac25/libindy/src/api/anoncreds.rs#L1433-L1445
*/
export class RequestedCredentials {
public constructor(options: RequestedCredentialsOptions) {
public constructor(options: RequestedCredentialsOptions = {}) {
if (options) {
this.requestedAttributes = options.requestedAttributes ?? {}
this.requestedPredicates = options.requestedPredicates ?? {}
Expand Down
8 changes: 7 additions & 1 deletion src/modules/proofs/models/RequestedPredicate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Expose } from 'class-transformer'
import { Exclude, Expose } from 'class-transformer'
import { IsInt, IsOptional, IsPositive, IsString } from 'class-validator'

import { IndyCredentialInfo } from '../../credentials'

/**
* Requested Predicate for Indy proof creation
*/
Expand All @@ -9,6 +11,7 @@ export class RequestedPredicate {
if (options) {
this.credentialId = options.credentialId
this.timestamp = options.timestamp
this.credentialInfo = options.credentialInfo
}
}

Expand All @@ -21,4 +24,7 @@ export class RequestedPredicate {
@IsInt()
@IsOptional()
public timestamp?: number

@Exclude({ toPlainOnly: true })
public credentialInfo!: IndyCredentialInfo
}
20 changes: 20 additions & 0 deletions src/modules/proofs/models/RetrievedCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { RequestedAttribute } from './RequestedAttribute'
import type { RequestedPredicate } from './RequestedPredicate'

export interface RetrievedCredentialsOptions {
requestedAttributes?: Record<string, RequestedAttribute[]>
requestedPredicates?: Record<string, RequestedPredicate[]>
}

/**
* Lists of requested credentials for Indy proof creation
*/
export class RetrievedCredentials {
public requestedAttributes: Record<string, RequestedAttribute[]>
public requestedPredicates: Record<string, RequestedPredicate[]>

public constructor(options: RetrievedCredentialsOptions = {}) {
this.requestedAttributes = options.requestedAttributes ?? {}
this.requestedPredicates = options.requestedPredicates ?? {}
}
}
1 change: 1 addition & 0 deletions src/modules/proofs/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './RequestedAttribute'
export * from './RequestedCredentials'
export * from './RequestedPredicate'
export * from './RequestedProof'
export * from './RetrievedCredentials'
139 changes: 64 additions & 75 deletions src/modules/proofs/services/ProofService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { JsonTransformer } from '../../../utils/JsonTransformer'
import { uuid } from '../../../utils/uuid'
import { Wallet } from '../../../wallet/Wallet'
import { AckStatus } from '../../common'
import { CredentialUtils, Credential, IndyCredentialInfo } from '../../credentials'
import { CredentialUtils, Credential } from '../../credentials'
import { IndyHolderService, IndyVerifierService } from '../../indy'
import { LedgerService } from '../../ledger/services/LedgerService'
import { ProofEventTypes } from '../ProofEvents'
Expand All @@ -41,6 +41,7 @@ import {
RequestedCredentials,
RequestedAttribute,
RequestedPredicate,
RetrievedCredentials,
} from '../models'
import { ProofRepository } from '../repository'
import { ProofRecord } from '../repository/ProofRecord'
Expand Down Expand Up @@ -611,110 +612,104 @@ export class ProofService {
}

/**
* Create a {@link RequestedCredentials} object. Given input proof request and presentation proposal,
* Create a {@link RetrievedCredentials} object. Given input proof request and presentation proposal,
* use credentials in the wallet to build indy requested credentials object for input to proof creation.
* If restrictions allow, self attested attributes will be used.
*
* Use the return value of this method as input to {@link ProofService.createPresentation} to automatically
* accept a received presentation request.
*
* @param proofRequest The proof request to build the requested credentials object from
* @param presentationProposal Optional presentation proposal to improve credential selection algorithm
* @returns Requested credentials object for use in proof creation
* @returns RetrievedCredentials object
*/
public async getRequestedCredentialsForProofRequest(
proofRequest: ProofRequest,
presentationProposal?: PresentationPreview
): Promise<RequestedCredentials> {
const requestedCredentials = new RequestedCredentials({})
): Promise<RetrievedCredentials> {
const retrievedCredentials = new RetrievedCredentials({})

for (const [referent, requestedAttribute] of Object.entries(proofRequest.requestedAttributes)) {
let credentialMatch: Credential | null = null
let credentialMatch: Credential[] = []
const credentials = await this.getCredentialsForProofRequest(proofRequest, referent)

// Can't construct without matching credentials
if (credentials.length === 0) {
throw new AriesFrameworkError(
`Could not automatically construct requested credentials for proof request '${proofRequest.name}'`
)
}
// If we have exactly one credential, or no proposal to pick preferences
// on the credential to use, we will use the first one
else if (credentials.length === 1 || !presentationProposal) {
credentialMatch = credentials[0]
// on the credentials to use, we will use the first one
if (credentials.length === 1 || !presentationProposal) {
credentialMatch = credentials
}
// If we have a proposal we will use that to determine the credential to use
// If we have a proposal we will use that to determine the credentials to use
else {
const names = requestedAttribute.names ?? [requestedAttribute.name]

// Find credential that matches all parameters from the proposal
for (const credential of credentials) {
// Find credentials that matches all parameters from the proposal
credentialMatch = credentials.filter((credential) => {
const { attributes, credentialDefinitionId } = credential.credentialInfo

// Check if credential matches all parameters from proposal
const isMatch = names.every((name) =>
// Check if credentials matches all parameters from proposal
return names.every((name) =>
presentationProposal.attributes.find(
(a) =>
a.name === name &&
a.credentialDefinitionId === credentialDefinitionId &&
(!a.value || a.value === attributes[name])
)
)

if (isMatch) {
credentialMatch = credential
break
}
}

if (!credentialMatch) {
throw new AriesFrameworkError(
`Could not automatically construct requested credentials for proof request '${proofRequest.name}'`
)
}
})
}

if (requestedAttribute.restrictions) {
requestedCredentials.requestedAttributes[referent] = new RequestedAttribute({
credentialId: credentialMatch.credentialInfo.referent,
retrievedCredentials.requestedAttributes[referent] = credentialMatch.map((credential: Credential) => {
return new RequestedAttribute({
credentialId: credential.credentialInfo.referent,
revealed: true,
credentialInfo: credential.credentialInfo,
})
}
// If there are no restrictions we can self attest the attribute
else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const value = credentialMatch.credentialInfo.attributes[requestedAttribute.name!]

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
requestedCredentials.selfAttestedAttributes[referent] = value!
}
})
}

for (const [referent, requestedPredicate] of Object.entries(proofRequest.requestedPredicates)) {
for (const [referent] of Object.entries(proofRequest.requestedPredicates)) {
const credentials = await this.getCredentialsForProofRequest(proofRequest, referent)

// Can't create requestedPredicates without matching credentials
if (credentials.length === 0) {
throw new AriesFrameworkError(
`Could not automatically construct requested credentials for proof request '${proofRequest.name}'`
)
}

const credentialMatch = credentials[0]
if (requestedPredicate.restrictions) {
requestedCredentials.requestedPredicates[referent] = new RequestedPredicate({
credentialId: credentialMatch.credentialInfo.referent,
retrievedCredentials.requestedPredicates[referent] = credentials.map((credential) => {
return new RequestedPredicate({
credentialId: credential.credentialInfo.referent,
credentialInfo: credential.credentialInfo,
})
})
}

return retrievedCredentials
}

/**
* Takes a RetrievedCredentials object and auto selects credentials in a RequestedCredentials object
*
* Use the return value of this method as input to {@link ProofService.createPresentation} to
* automatically accept a received presentation request.
*
* @param retrievedCredentials The retrieved credentials object to get credentials from
*
* @returns RequestedCredentials
*/
public autoSelectCredentialsForProofRequest(retrievedCredentials: RetrievedCredentials): RequestedCredentials {
const requestedCredentials = new RequestedCredentials({})

Object.keys(retrievedCredentials.requestedAttributes).forEach((attributeName) => {
const attributeArray = retrievedCredentials.requestedAttributes[attributeName]

if (attributeArray.length === 0) {
throw new AriesFrameworkError('Unable to automatically select requested attributes.')
} else {
requestedCredentials.requestedAttributes[attributeName] = attributeArray[0]
}
// If there are no restrictions we can self attest the attribute
else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const value = credentialMatch.credentialInfo.attributes[requestedPredicate.name!]
})

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
requestedCredentials.selfAttestedAttributes[referent] = value!
Object.keys(retrievedCredentials.requestedPredicates).forEach((attributeName) => {
if (retrievedCredentials.requestedPredicates[attributeName].length === 0) {
throw new AriesFrameworkError('Unable to automatically select requested predicates.')
} else {
requestedCredentials.requestedPredicates[attributeName] =
retrievedCredentials.requestedPredicates[attributeName][0]
}
}
})

return requestedCredentials
}
Expand Down Expand Up @@ -814,16 +809,10 @@ export class ProofService {
proofRequest: ProofRequest,
requestedCredentials: RequestedCredentials
): Promise<IndyProof> {
const credentialObjects: IndyCredentialInfo[] = []

for (const credentialId of requestedCredentials.getCredentialIdentifiers()) {
const credentialInfo = JsonTransformer.fromJSON(
await this.indyHolderService.getCredential(credentialId),
IndyCredentialInfo
)

credentialObjects.push(credentialInfo)
}
const credentialObjects = [
...Object.values(requestedCredentials.requestedAttributes),
...Object.values(requestedCredentials.requestedPredicates),
].map((c) => c.credentialInfo)

const schemas = await this.getSchemas(new Set(credentialObjects.map((c) => c.schemaId)))
const credentialDefinitions = await this.getCredentialDefinitions(
Expand Down

0 comments on commit 012afa6

Please sign in to comment.