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: add oob proof proposal #1370

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol<

// Create record
const proofRecord = new ProofExchangeRecord({
connectionId: connectionRecord.id,
connectionId: connectionRecord?.id,
threadId: message.threadId,
parentThreadId: message.thread?.parentThreadId,
state: ProofState.ProposalSent,
Expand Down Expand Up @@ -416,6 +416,13 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol<

let proofRecord = await this.findByThreadAndConnectionId(agentContext, proofRequestMessage.threadId, connection?.id)

if (!proofRecord) {
// Proof request bound to a proofRecord by threadId: proof proposal in OOB msg
// TODO integrate with oob module
proofRecord = await this.findByThreadAndConnectionId(messageContext.agentContext, proofRequestMessage.threadId)
if (proofRecord) proofRecord.connectionId = connection?.id
}

const requestAttachment = proofRequestMessage.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID)
if (!requestAttachment) {
throw new AriesFrameworkError(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import type { EventReplaySubject } from '../../../../../../core/tests'
import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup'

import { ProofState } from '../../../../../../core/src'
import { testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests'
import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup'
import { waitForProofExchangeRecordSubject, testLogger, waitForProofExchangeRecord } from '../../../../../../core/tests'
import { issueLegacyAnonCredsCredential, setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup'
import { V1PresentationMessage, V1ProposePresentationMessage, V1RequestPresentationMessage } from '../messages'

describe('Present Proof', () => {
let faberAgent: AnonCredsTestsAgent
let faberReplay: EventReplaySubject
let aliceAgent: AnonCredsTestsAgent
let aliceReplay: EventReplaySubject
let aliceConnectionId: string
let faberConnectionId: string
let credentialDefinitionId: string

beforeAll(async () => {
testLogger.test('Initializing the agents')
;({
issuerAgent: faberAgent,
issuerReplay: faberReplay,
holderAgent: aliceAgent,
holderReplay: aliceReplay,
credentialDefinitionId,
holderIssuerConnectionId: aliceConnectionId,
issuerHolderConnectionId: faberConnectionId,
} = await setupAnonCredsTests({
issuerName: 'Faber - V1 Indy Proof Request',
holderName: 'Alice - V1 Indy Proof Request',
Expand Down Expand Up @@ -103,4 +111,114 @@ describe('Present Proof', () => {
protocolVersion: 'v1',
})
})

test('Alice Creates oob proof proposal for Faber', async () => {
await issueLegacyAnonCredsCredential({
issuerAgent: faberAgent,
issuerReplay: faberReplay,
holderAgent: aliceAgent,
holderReplay: aliceReplay,
issuerHolderConnectionId: faberConnectionId,
offer: {
credentialDefinitionId,
attributes: [
{
name: 'name',
value: 'Alice',
},
{
name: 'age',
value: '99',
},
],
},
})
testLogger.test('Alice creates oob proof proposal for Faber')
const { message } = await aliceAgent.proofs.createProofProposal({
protocolVersion: 'v1',
proofFormats: {
indy: {
name: 'ProofRequest',
version: '1.0',
attributes: [
{
name: 'name',
value: 'John',
credentialDefinitionId,
referent: '0',
},
],
predicates: [
{
name: 'age',
predicate: '>=',
threshold: 50,
credentialDefinitionId,
},
],
},
},
comment: 'V1 propose proof test',
})
const { outOfBandInvitation } = await aliceAgent.oob.createInvitation({
messages: [message],
autoAcceptConnection: true,
})
await faberAgent.oob.receiveInvitation(outOfBandInvitation)
testLogger.test('Faber waits for proof proposal message from Alice')
let faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, {
state: ProofState.ProposalReceived,
})

// Faber accepts the presentation proposal from Alice
testLogger.test('Faber accepts presentation proposal from Alice')
faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({
proofRecordId: faberProofExchangeRecord.id,
})

// ALice waits for presentation request from Faber
testLogger.test('Alice waits for presentation request from Faber')
let aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, {
state: ProofState.RequestReceived,
})
expect(aliceProofExchangeRecord.connectionId).not.toBeNull()

// Alice retrieves the requested credentials and accepts the presentation
testLogger.test('Alice accepts presentation request from Faber')
const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({
proofRecordId: aliceProofExchangeRecord.id,
})
await aliceAgent.proofs.acceptRequest({
proofRecordId: aliceProofExchangeRecord.id,
proofFormats: { indy: requestedCredentials.proofFormats.indy },
})

// Faber waits for the presentation from Alice
testLogger.test('Faber waits for presentation from Alice')
faberProofExchangeRecord = await waitForProofExchangeRecordSubject(faberReplay, {
threadId: aliceProofExchangeRecord.threadId,
state: ProofState.PresentationReceived,
})

// Faber accepts the presentation provided by Alice
testLogger.test('Faber accepts the presentation provided by Alice')
await faberAgent.proofs.acceptPresentation({
proofRecordId: faberProofExchangeRecord.id,
})

// Alice waits utils she received a presentation acknowledgement
testLogger.test('Alice waits until she receives a presentation acknowledgement')
aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, {
threadId: aliceProofExchangeRecord.threadId,
state: ProofState.Done,
})

const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id)
const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id)
const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id)

expect(proposalMessage).toBeInstanceOf(V1ProposePresentationMessage)
expect(requestMessage).toBeInstanceOf(V1RequestPresentationMessage)
expect(presentationMessage).toBeInstanceOf(V1PresentationMessage)
})
})
25 changes: 25 additions & 0 deletions packages/core/src/modules/proofs/ProofsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
AcceptProofOptions,
AcceptProofProposalOptions,
AcceptProofRequestOptions,
CreateProofProposalOptions,
CreateProofRequestOptions,
DeleteProofOptions,
FindProofPresentationMessageReturn,
Expand Down Expand Up @@ -60,6 +61,10 @@ export interface ProofsApi<PPs extends ProofProtocol[]> {
message: AgentMessage
proofRecord: ProofExchangeRecord
}>
createProofProposal(options: CreateProofProposalOptions<PPs>): Promise<{
message: AgentMessage
proofRecord: ProofExchangeRecord
}>

// Auto Select
selectCredentialsForRequest(
Expand Down Expand Up @@ -164,6 +169,26 @@ export class ProofsApi<PPs extends ProofProtocol[]> implements ProofsApi<PPs> {
return proofRecord
}

/**
* Initiate a new presentation exchange as prover by sending an out of band proof proposal message
*
* @param options multiple properties like protocol version, proof Formats to build the proof request
* @returns the message itself and the proof record associated with the sent request message
*/
public async createProofProposal(options: CreateProofProposalOptions<PPs>): Promise<{
message: AgentMessage
proofRecord: ProofExchangeRecord
}> {
const protocol = this.getProtocol(options.protocolVersion)
return await protocol.createProposal(this.agentContext, {
proofFormats: options.proofFormats,
autoAcceptProof: options.autoAcceptProof,
goalCode: options.goalCode,
comment: options.comment,
parentThreadId: options.parentThreadId,
})
}

/**
* Accept a presentation proposal as verifier (by sending a presentation request message) to the connection
* associated with the proof record.
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/modules/proofs/ProofsApiOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ export interface ProposeProofOptions<PPs extends ProofProtocol[] = ProofProtocol
parentThreadId?: string
}

/**
* Interface for ProofsApi.createProofProposal. Will create an out of band proposal.
*/
export interface CreateProofProposalOptions<PPs extends ProofProtocol[] = ProofProtocol[]> extends BaseOptions {
protocolVersion: ProofsProtocolVersionType<PPs>
proofFormats: ProofFormatPayload<ProofFormatsFromProtocols<PPs>, 'createProposal'>

goalCode?: string
parentThreadId?: string
}

/**
* Interface for ProofsApi.acceptProposal. Will send a request
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ interface BaseOptions {
}

export interface CreateProofProposalOptions<PFs extends ProofFormatService[]> extends BaseOptions {
connectionRecord: ConnectionRecord
connectionRecord?: ConnectionRecord
proofFormats: ProofFormatPayload<ExtractProofFormats<PFs>, 'createProposal'>
parentThreadId?: string
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class V2ProofProtocol<PFs extends ProofFormatService[] = ProofFormatServi
}

const proofRecord = new ProofExchangeRecord({
connectionId: connectionRecord.id,
connectionId: connectionRecord?.id,
threadId: uuid(),
parentThreadId,
state: ProofState.ProposalSent,
Expand Down Expand Up @@ -396,6 +396,13 @@ export class V2ProofProtocol<PFs extends ProofFormatService[] = ProofFormatServi
connection?.id
)

if (!proofRecord) {
// Proof request bound to a proofRecord by threadId: proof proposal in OOB msg
// TODO integrate with oob module
proofRecord = await this.findByThreadAndConnectionId(messageContext.agentContext, requestMessage.threadId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments here

if (proofRecord) proofRecord.connectionId = connection?.id
}

const formatServices = this.getFormatServicesFromMessage(requestMessage.formats)
if (formatServices.length === 0) {
throw new AriesFrameworkError(`Unable to process request. No supported formats`)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { Agent } from '../../../../../agent/Agent'

import {
setupAnonCredsTests,
issueLegacyAnonCredsCredential,
} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup'
import { testLogger, waitForProofExchangeRecordSubject } from '../../../../../../tests'
import { AutoAcceptProof, ProofState } from '../../../models'
import { V2PresentationMessage, V2ProposePresentationMessage, V2RequestPresentationMessage } from '../messages'

describe('V2 OOB Proposal Proposal - Indy', () => {
let agents: Agent[]

afterEach(async () => {
for (const agent of agents) {
await agent.shutdown()
await agent.wallet.delete()
}
})

test('Alice start with oob proof proposal for Faber with aut-accept enabled', async () => {
const {
issuerAgent: faberAgent,
issuerReplay: faberReplay,
holderAgent: aliceAgent,
holderReplay: aliceReplay,
credentialDefinitionId,
issuerHolderConnectionId: faberConnectionId,
} = await setupAnonCredsTests({
issuerName: 'Faber oob Proofs proposal v2 - Auto Accept',
holderName: 'Alice oob Proofs proposal v2 - Auto Accept',
autoAcceptProofs: AutoAcceptProof.Always,
attributeNames: ['name', 'age'],
})
await issueLegacyAnonCredsCredential({
issuerAgent: faberAgent,
issuerReplay: faberReplay,
holderAgent: aliceAgent,
holderReplay: aliceReplay,
issuerHolderConnectionId: faberConnectionId,
offer: {
credentialDefinitionId,
attributes: [
{
name: 'name',
value: 'Alice',
},
{
name: 'age',
value: '99',
},
],
},
})
agents = [aliceAgent, faberAgent]
testLogger.test('Alice creates oob proof proposal for faber')
const { message } = await aliceAgent.proofs.createProofProposal({
protocolVersion: 'v2',
proofFormats: {
indy: {
name: 'abc',
version: '1.0',
attributes: [
{
name: 'name',
value: 'Alice',
credentialDefinitionId,
},
],
predicates: [
{
name: 'age',
predicate: '>=',
threshold: 50,
credentialDefinitionId,
},
],
},
},
autoAcceptProof: AutoAcceptProof.ContentApproved,
})
const { outOfBandInvitation } = await aliceAgent.oob.createInvitation({
messages: [message],
autoAcceptConnection: true,
})
await faberAgent.oob.receiveInvitation(outOfBandInvitation)

const aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, {
state: ProofState.Done,
})
await waitForProofExchangeRecordSubject(faberReplay, {
state: ProofState.Done,
})
expect(aliceProofExchangeRecord.connectionId).not.toBeNull()
const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id)
const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id)
const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id)
expect(proposalMessage).toBeInstanceOf(V2ProposePresentationMessage)
expect(requestMessage).toBeInstanceOf(V2RequestPresentationMessage)
expect(presentationMessage).toBeInstanceOf(V2PresentationMessage)
})
})