Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: method to retrieve credentials for proof request #329

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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