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: issue credentials v2 (W3C/JSON-LD) #1092

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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ coverage
.DS_Store
logs.txt
logs/
packages/core/src/__tests__/genesis-von.txn
lerna-debug.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
import type { Agent } from '../../core/src/agent/Agent'
import type { ConnectionRecord } from '../../core/src/modules/connections'
import type { SignCredentialOptionsRFC0593 } from '../../core/src/modules/credentials/formats/jsonld'
import type { Wallet } from '../../core/src/wallet'

import { InjectionSymbols } from '../../core/src/constants'
import { KeyType } from '../../core/src/crypto'
import { CredentialState } from '../../core/src/modules/credentials/models'
import { V2IssueCredentialMessage } from '../../core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage'
import { V2OfferCredentialMessage } from '../../core/src/modules/credentials/protocol/v2/messages/V2OfferCredentialMessage'
import { CredentialExchangeRecord } from '../../core/src/modules/credentials/repository/CredentialExchangeRecord'
import { DidKey } from '../../core/src/modules/dids'
import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '../../core/src/modules/vc'
import { W3cCredential } from '../../core/src/modules/vc/models/credential/W3cCredential'
import { DidCommMessageRepository } from '../../core/src/storage'
import { JsonTransformer } from '../../core/src/utils/JsonTransformer'
import { setupCredentialTests, waitForCredentialRecord } from '../../core/tests/helpers'
import testLogger from '../../core/tests/logger'

import { describeSkipNode17And18 } from './util'

let faberAgent: Agent
let aliceAgent: Agent
let aliceConnection: ConnectionRecord
let aliceCredentialRecord: CredentialExchangeRecord
let faberCredentialRecord: CredentialExchangeRecord

const TEST_LD_DOCUMENT = {
'@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL],
id: 'https://issuer.oidp.uscis.gov/credentials/83627465',
type: ['VerifiableCredential', 'PermanentResidentCard'],
issuer: '',
identifier: '83627465',
name: 'Permanent Resident Card',
description: 'Government of Example Permanent Resident Card.',
issuanceDate: '2019-12-03T12:19:52Z',
expirationDate: '2029-12-03T12:19:52Z',
credentialSubject: {
id: 'did:example:b34ca6cd37bbf23',
type: ['PermanentResident', 'Person'],
givenName: 'JOHN',
familyName: 'SMITH',
gender: 'Male',
image: 'data:image/png;base64,iVBORw0KGgokJggg==',
residentSince: '2015-01-01',
lprCategory: 'C09',
lprNumber: '999-999-999',
commuterClassification: 'C1',
birthCountry: 'Bahamas',
birthDate: '1958-07-17',
},
}

describeSkipNode17And18('credentials, BBS+ signature', () => {
let wallet
let issuerDidKey: DidKey
let didCommMessageRepository: DidCommMessageRepository
let signCredentialOptions: SignCredentialOptionsRFC0593
const seed = 'testseed000000000000000000000001'
beforeAll(async () => {
;({ faberAgent, aliceAgent, aliceConnection } = await setupCredentialTests(
'Faber Agent Credentials LD BBS+',
'Alice Agent Credentials LD BBS+'
))
wallet = faberAgent.injectionContainer.resolve<Wallet>(InjectionSymbols.Wallet)
await wallet.createDid({ seed })
const key = await wallet.createKey({ keyType: KeyType.Bls12381g2, seed })

issuerDidKey = new DidKey(key)
})

afterAll(async () => {
await faberAgent.shutdown()
await faberAgent.wallet.delete()
await aliceAgent.shutdown()
await aliceAgent.wallet.delete()
})

test('Alice starts with V2 (ld format, BbsBlsSignature2020 signature) credential proposal to Faber', async () => {
testLogger.test('Alice sends (v2 jsonld) credential proposal to Faber')
// set the propose options

const credentialJson = TEST_LD_DOCUMENT
credentialJson.issuer = issuerDidKey.did

const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential)

signCredentialOptions = {
credential,
options: {
proofType: 'BbsBlsSignature2020',
proofPurpose: 'assertionMethod',
},
}

testLogger.test('Alice sends (v2, Indy) credential proposal to Faber')

const credentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({
connectionId: aliceConnection.id,
protocolVersion: 'v2',
credentialFormats: {
jsonld: signCredentialOptions,
},
comment: 'v2 propose credential test for W3C Credentials',
})

expect(credentialExchangeRecord.connectionId).toEqual(aliceConnection.id)
expect(credentialExchangeRecord.protocolVersion).toEqual('v2')
expect(credentialExchangeRecord.state).toEqual(CredentialState.ProposalSent)
expect(credentialExchangeRecord.threadId).not.toBeNull()

testLogger.test('Faber waits for credential proposal from Alice')
faberCredentialRecord = await waitForCredentialRecord(faberAgent, {
threadId: credentialExchangeRecord.threadId,
state: CredentialState.ProposalReceived,
})
testLogger.test('Faber sends credential offer to Alice')
await faberAgent.credentials.acceptProposal({
credentialRecordId: faberCredentialRecord.id,
comment: 'V2 W3C Offer',
credentialFormats: {
jsonld: signCredentialOptions,
},
})

testLogger.test('Alice waits for credential offer from Faber')
aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, {
threadId: faberCredentialRecord.threadId,
state: CredentialState.OfferReceived,
})

didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository)

const offerMessage = await didCommMessageRepository.findAgentMessage(aliceAgent.context, {
associatedRecordId: aliceCredentialRecord.id,
messageClass: V2OfferCredentialMessage,
})

expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({
'@type': 'https://didcomm.org/issue-credential/2.0/offer-credential',
'@id': expect.any(String),
comment: 'V2 W3C Offer',
formats: [
{
attach_id: expect.any(String),
format: 'aries/ld-proof-vc-detail@v1.0',
},
],
'offers~attach': [
{
'@id': expect.any(String),
'mime-type': 'application/json',
data: expect.any(Object),
lastmod_time: undefined,
byte_count: undefined,
},
],
'~thread': {
thid: expect.any(String),
pthid: undefined,
sender_order: undefined,
received_orders: undefined,
},
'~service': undefined,
'~attach': undefined,
'~please_ack': undefined,
'~timing': undefined,
'~transport': undefined,
'~l10n': undefined,
credential_preview: expect.any(Object),
replacement_id: undefined,
})
expect(aliceCredentialRecord.id).not.toBeNull()
expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type)

if (!aliceCredentialRecord.connectionId) {
throw new Error('Missing Connection Id')
}

const offerCredentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({
credentialRecordId: aliceCredentialRecord.id,
credentialFormats: {
jsonld: undefined,
},
})

expect(offerCredentialExchangeRecord.connectionId).toEqual(aliceConnection.id)
expect(offerCredentialExchangeRecord.protocolVersion).toEqual('v2')
expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent)
expect(offerCredentialExchangeRecord.threadId).not.toBeNull()

testLogger.test('Faber waits for credential request from Alice')
await waitForCredentialRecord(faberAgent, {
threadId: aliceCredentialRecord.threadId,
state: CredentialState.RequestReceived,
})

testLogger.test('Faber sends credential to Alice')

await faberAgent.credentials.acceptRequest({
credentialRecordId: faberCredentialRecord.id,
comment: 'V2 W3C Offer',
credentialFormats: {
jsonld: signCredentialOptions,
},
})

testLogger.test('Alice waits for credential from Faber')
aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, {
threadId: faberCredentialRecord.threadId,
state: CredentialState.CredentialReceived,
})

testLogger.test('Alice sends credential ack to Faber')
await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id })

testLogger.test('Faber waits for credential ack from Alice')
faberCredentialRecord = await waitForCredentialRecord(faberAgent, {
threadId: faberCredentialRecord.threadId,
state: CredentialState.Done,
})
expect(aliceCredentialRecord).toMatchObject({
type: CredentialExchangeRecord.type,
id: expect.any(String),
createdAt: expect.any(Date),
threadId: expect.any(String),
connectionId: expect.any(String),
state: CredentialState.CredentialReceived,
})

const credentialMessage = await didCommMessageRepository.getAgentMessage(faberAgent.context, {
associatedRecordId: faberCredentialRecord.id,
messageClass: V2IssueCredentialMessage,
})

const w3cCredential = credentialMessage.credentialAttachments[0].getDataAsJson()

expect(w3cCredential).toMatchObject({
context: [
'https://www.w3.org/2018/credentials/v1',
'https://w3id.org/citizenship/v1',
'https://w3id.org/security/bbs/v1',
],
id: 'https://issuer.oidp.uscis.gov/credentials/83627465',
type: ['VerifiableCredential', 'PermanentResidentCard'],
issuer:
'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa',
identifier: '83627465',
name: 'Permanent Resident Card',
description: 'Government of Example Permanent Resident Card.',
issuanceDate: '2019-12-03T12:19:52Z',
expirationDate: '2029-12-03T12:19:52Z',
credentialSubject: {
id: 'did:example:b34ca6cd37bbf23',
type: ['PermanentResident', 'Person'],
givenName: 'JOHN',
familyName: 'SMITH',
gender: 'Male',
image: 'data:image/png;base64,iVBORw0KGgokJggg==',
residentSince: '2015-01-01',
lprCategory: 'C09',
lprNumber: '999-999-999',
commuterClassification: 'C1',
birthCountry: 'Bahamas',
birthDate: '1958-07-17',
},
proof: {
type: 'BbsBlsSignature2020',
created: expect.any(String),
verificationMethod:
'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa',
proofPurpose: 'assertionMethod',
proofValue: expect.any(String),
},
})

expect(JsonTransformer.toJSON(credentialMessage)).toMatchObject({
'@type': 'https://didcomm.org/issue-credential/2.0/issue-credential',
'@id': expect.any(String),
comment: 'V2 W3C Offer',
formats: [
{
attach_id: expect.any(String),
format: 'aries/ld-proof-vc@1.0',
},
],
'credentials~attach': [
{
'@id': expect.any(String),
'mime-type': 'application/json',
data: expect.any(Object),
lastmod_time: undefined,
byte_count: undefined,
},
],
'~thread': {
thid: expect.any(String),
pthid: undefined,
sender_order: undefined,
received_orders: undefined,
},
'~please_ack': { on: ['RECEIPT'] },
'~service': undefined,
'~attach': undefined,
'~timing': undefined,
'~transport': undefined,
'~l10n': undefined,
})
})
})
3 changes: 2 additions & 1 deletion packages/core/src/modules/credentials/CredentialsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
} from './CredentialsApiOptions'
import type { CredentialFormat } from './formats'
import type { IndyCredentialFormat } from './formats/indy/IndyCredentialFormat'
import type { JsonLdCredentialFormat } from './formats/jsonld/JsonLdCredentialFormat'
import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord'
import type { CredentialService } from './services/CredentialService'

Expand Down Expand Up @@ -92,7 +93,7 @@ export interface CredentialsApi<CFs extends CredentialFormat[], CSs extends Cred

@injectable()
export class CredentialsApi<
CFs extends CredentialFormat[] = [IndyCredentialFormat],
CFs extends CredentialFormat[] = [IndyCredentialFormat, JsonLdCredentialFormat],
CSs extends CredentialService<CFs>[] = [V1CredentialService, V2CredentialService<CFs>]
> implements CredentialsApi<CFs, CSs>
{
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/modules/credentials/CredentialsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Protocol } from '../../agent/models'
import { CredentialsApi } from './CredentialsApi'
import { CredentialsModuleConfig } from './CredentialsModuleConfig'
import { IndyCredentialFormatService } from './formats/indy'
import { JsonLdCredentialFormatService } from './formats/jsonld/JsonLdCredentialFormatService'
import { RevocationNotificationService } from './protocol/revocation-notification/services'
import { V1CredentialService } from './protocol/v1'
import { V2CredentialService } from './protocol/v2'
Expand Down Expand Up @@ -60,5 +61,6 @@ export class CredentialsModule implements Module {

// Credential Formats
dependencyManager.registerSingleton(IndyCredentialFormatService)
dependencyManager.registerSingleton(JsonLdCredentialFormatService)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CredentialsApi } from '../CredentialsApi'
import { CredentialsModule } from '../CredentialsModule'
import { CredentialsModuleConfig } from '../CredentialsModuleConfig'
import { IndyCredentialFormatService } from '../formats'
import { JsonLdCredentialFormatService } from '../formats/jsonld/JsonLdCredentialFormatService'
import { V1CredentialService, V2CredentialService } from '../protocol'
import { RevocationNotificationService } from '../protocol/revocation-notification/services'
import { CredentialRepository } from '../repository'
Expand All @@ -29,11 +30,13 @@ describe('CredentialsModule', () => {
expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1)
expect(dependencyManager.registerInstance).toHaveBeenCalledWith(CredentialsModuleConfig, credentialsModule.config)

expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(5)
expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(6)

expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V1CredentialService)
expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(V2CredentialService)
expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(RevocationNotificationService)
expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(CredentialRepository)
expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(IndyCredentialFormatService)
expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(JsonLdCredentialFormatService)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
FormatAutoRespondOfferOptions,
FormatAutoRespondProposalOptions,
FormatAutoRespondRequestOptions,
FormatProcessCredentialOptions,
} from './CredentialFormatServiceOptions'

import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment'
Expand Down Expand Up @@ -58,7 +59,7 @@ export abstract class CredentialFormatService<CF extends CredentialFormat = Cred
): Promise<CredentialFormatCreateReturn>

// credential methods
abstract processCredential(agentContext: AgentContext, options: FormatProcessOptions): Promise<void>
abstract processCredential(agentContext: AgentContext, options: FormatProcessCredentialOptions): Promise<void>

// auto accept methods
abstract shouldAutoRespondToProposal(agentContext: AgentContext, options: FormatAutoRespondProposalOptions): boolean
Expand Down
Loading