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 credential info to access attributes #254

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
19 changes: 14 additions & 5 deletions src/__tests__/credentials.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable no-console */
import indy from 'indy-sdk'
import type { CredDefId } from 'indy-sdk'
import { Subject } from 'rxjs'
import { Agent, ConnectionRecord } from '..'
import {
Expand Down Expand Up @@ -65,7 +64,8 @@ const credentialPreview = new CredentialPreview({
describe('credentials', () => {
let faberAgent: Agent
let aliceAgent: Agent
let credDefId: CredDefId
let credDefId: string
let schemaId: string
let faberConnection: ConnectionRecord
let aliceConnection: ConnectionRecord
let faberCredentialRecord: CredentialRecord
Expand All @@ -90,7 +90,8 @@ describe('credentials', () => {
attributes: ['name', 'age'],
version: '1.0',
}
const [, ledgerSchema] = await registerSchema(faberAgent, schemaTemplate)
const [ledgerSchemaId, ledgerSchema] = await registerSchema(faberAgent, schemaTemplate)
schemaId = ledgerSchemaId

const definitionTemplate = {
schema: ledgerSchema,
Expand Down Expand Up @@ -204,7 +205,11 @@ describe('credentials', () => {
},
offerMessage: expect.any(Object),
requestMessage: expect.any(Object),
requestMetadata: expect.any(Object),
metadata: {
requestMetadata: expect.any(Object),
schemaId,
credentialDefinitionId: credDefId,
},
credentialId: expect.any(String),
state: CredentialState.Done,
})
Expand All @@ -216,6 +221,10 @@ describe('credentials', () => {
tags: {
threadId: expect.any(String),
},
metadata: {
schemaId,
credentialDefinitionId: credDefId,
},
offerMessage: expect.any(Object),
requestMessage: expect.any(Object),
state: CredentialState.Done,
Expand Down Expand Up @@ -303,7 +312,7 @@ describe('credentials', () => {
},
offerMessage: expect.any(Object),
requestMessage: expect.any(Object),
requestMetadata: expect.any(Object),
metadata: { requestMetadata: expect.any(Object) },
credentialId: expect.any(String),
state: CredentialState.Done,
})
Expand Down
44 changes: 40 additions & 4 deletions src/modules/credentials/CredentialUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CredValues } from 'indy-sdk'
import { sha256 } from 'js-sha256'
import BigNumber from 'bn.js'

import { CredentialPreview } from './messages/CredentialPreview'
import { CredentialPreviewAttribute } from './messages/CredentialPreview'

export class CredentialUtils {
/**
Expand All @@ -11,12 +11,12 @@ export class CredentialUtils {
* - hash with sha256,
* - convert to byte array and reverse it
* - convert it to BigInteger and return as a string
* @param credentialPreview
* @param attributes
*
* @returns CredValues
*/
public static convertPreviewToValues(credentialPreview: CredentialPreview): CredValues {
return credentialPreview.attributes.reduce((credentialValues, attribute) => {
public static convertAttributesToValues(attributes: CredentialPreviewAttribute[]): CredValues {
return attributes.reduce((credentialValues, attribute) => {
return {
[attribute.name]: {
raw: attribute.value,
Expand All @@ -27,6 +27,42 @@ export class CredentialUtils {
}, {})
}

/**
* Assert two credential values objects match.
*
* @param firstValues The first values object
* @param secondValues The second values object
*
* @throws If not all values match
*/
public static assertValuesMatch(firstValues: CredValues, secondValues: CredValues) {
const firstValuesKeys = Object.keys(firstValues)
const secondValuesKeys = Object.keys(secondValues)

if (firstValuesKeys.length !== secondValuesKeys.length) {
throw new Error(
`Number of values in first entry (${firstValuesKeys.length}) does not match number of values in second entry (${secondValuesKeys.length})`
)
}

for (const key of firstValuesKeys) {
const firstValue = firstValues[key]
const secondValue = secondValues[key]

if (!secondValue) {
throw new Error(`Second cred values object has not value for key '${key}'`)
}

if (firstValue.encoded !== secondValue.encoded) {
throw new Error(`Encoded credential values for key '${key}' do not match`)
}

if (firstValue.raw !== secondValue.raw) {
throw new Error(`Raw credential values for key '${key}' do not match`)
}
}
}

/**
* Check whether the raw value matches the encoded version according to the encoding format described in Aries RFC 0037
* Use this method to ensure the received proof (over the encoded) value is the same as the raw value of the data.
Expand Down
4 changes: 2 additions & 2 deletions src/modules/credentials/CredentialsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ConnectionService } from '../connections'
import { EventEmitter } from 'events'
import { CredentialOfferTemplate, CredentialService } from './services'
import { ProposeCredentialMessageOptions } from './messages'
import { CredentialInfo } from './models'
import { IndyCredentialInfo } from './models'
import { Dispatcher } from '../../agent/Dispatcher'
import {
ProposeCredentialHandler,
Expand Down Expand Up @@ -228,7 +228,7 @@ export class CredentialsModule {
* @param credentialId the id (referent) of the indy credential
* @returns Indy credential info object
*/
public async getIndyCredential(credentialId: string): Promise<CredentialInfo> {
public async getIndyCredential(credentialId: string): Promise<IndyCredentialInfo> {
return this.credentialService.getIndyCredential(credentialId)
}

Expand Down
24 changes: 24 additions & 0 deletions src/modules/credentials/__tests__/CredentialInfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CredentialInfo } from '../models/CredentialInfo'

describe('CredentialInfo', () => {
it('should return the correct property values', () => {
const claims = {
name: 'Timo',
date_of_birth: '1998-07-29',
'country-of-residence': 'The Netherlands',
'street name': 'Test street',
age: '22',
}
const metadata = {
credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG',
schemaId: 'TL1EaPFCZ8Si5aUrqScBDt:2:test-schema-1599055118161:1.0',
}
const credentialInfo = new CredentialInfo({
claims,
metadata,
})

expect(credentialInfo.claims).toEqual(claims)
expect(credentialInfo.metadata).toEqual(metadata)
})
})
33 changes: 33 additions & 0 deletions src/modules/credentials/__tests__/CredentialRecord.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CredentialRecord } from '../repository/CredentialRecord'
import { CredentialState } from '../CredentialState'
import { CredentialPreviewAttribute } from '../messages'

describe('CredentialRecord', () => {
describe('getCredentialInfo()', () => {
test('creates credential info object from credential record data', () => {
const credentialRecord = new CredentialRecord({
connectionId: '28790bfe-1345-4c64-b21a-7d98982b3894',
state: CredentialState.Done,
credentialAttributes: [
new CredentialPreviewAttribute({
name: 'age',
value: '25',
}),
],
metadata: {
credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG',
schemaId: 'TL1EaPFCZ8Si5aUrqScBDt:2:test-schema-1599055118161:1.0',
},
})

const credentialInfo = credentialRecord.getCredentialInfo()
expect(credentialInfo?.claims).toEqual({
age: '25',
})
expect(credentialInfo?.metadata).toEqual({
credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG',
schemaId: 'TL1EaPFCZ8Si5aUrqScBDt:2:test-schema-1599055118161:1.0',
})
})
})
})
45 changes: 32 additions & 13 deletions src/modules/credentials/__tests__/CredentialService.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable no-console */
import type Indy from 'indy-sdk'
import type { CredReqMetadata, WalletQuery, CredDef } from 'indy-sdk'
import type { WalletQuery, CredDef } from 'indy-sdk'
import { Wallet } from '../../../wallet/Wallet'
import { Repository } from '../../../storage/Repository'
import { CredentialOfferTemplate, CredentialService, CredentialEventType } from '../services'
import { CredentialRecord } from '../repository/CredentialRecord'
import { CredentialRecord, CredentialRecordMetadata, CredentialRecordTags } from '../repository/CredentialRecord'
import { InboundMessageContext } from '../../../agent/models/InboundMessageContext'
import { CredentialState } from '../CredentialState'
import { StubWallet } from './StubWallet'
Expand All @@ -27,14 +27,14 @@ import { LedgerService as LedgerServiceImpl } from '../../ledger/services'
import { ConnectionState } from '../../connections'
import { getMockConnection } from '../../connections/__tests__/ConnectionService.test'
import { AgentConfig } from '../../../agent/AgentConfig'
import { CredentialUtils } from '../CredentialUtils'

jest.mock('./../../../storage/Repository')
jest.mock('./../../../modules/ledger/services/LedgerService')

const indy = {} as typeof Indy

const CredentialRepository = <jest.Mock<Repository<CredentialRecord>>>(<unknown>Repository)
// const ConnectionService = <jest.Mock<ConnectionServiceImpl>>(<unknown>ConnectionServiceImpl);
const LedgerService = <jest.Mock<LedgerServiceImpl>>(<unknown>LedgerServiceImpl)

const connection = getMockConnection({
Expand Down Expand Up @@ -74,12 +74,13 @@ const requestAttachment = new Attachment({
}),
})

// TODO: replace attachment with credential fixture
const credentialAttachment = new Attachment({
id: INDY_CREDENTIAL_ATTACHMENT_ID,
mimeType: 'application/json',
data: new AttachmentData({
base64: JsonEncoder.toBase64(credReq),
base64: JsonEncoder.toBase64({
values: CredentialUtils.convertAttributesToValues(credentialPreview.attributes),
}),
}),
})

Expand All @@ -88,15 +89,17 @@ const credentialAttachment = new Attachment({
const mockCredentialRecord = ({
state,
requestMessage,
requestMetadata,
metadata,
tags,
id,
credentialAttributes,
}: {
state: CredentialState
requestMessage?: RequestCredentialMessage
requestMetadata?: CredReqMetadata
tags?: Record<string, unknown>
metadata?: CredentialRecordMetadata
tags?: CredentialRecordTags
id?: string
credentialAttributes?: CredentialPreviewAttribute[]
}) =>
new CredentialRecord({
offerMessage: new OfferCredentialMessage({
Expand All @@ -105,12 +108,13 @@ const mockCredentialRecord = ({
attachments: [offerAttachment],
}),
id,
credentialAttributes: credentialAttributes || credentialPreview.attributes,
requestMessage,
requestMetadata: requestMetadata,
metadata,
state: state || CredentialState.OfferSent,
tags: tags || {},
connectionId: '123',
} as any)
})

describe('CredentialService', () => {
let wallet: Wallet
Expand Down Expand Up @@ -320,7 +324,7 @@ describe('CredentialService', () => {
expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1)
const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls
expect(updatedCredentialRecord).toMatchObject({
requestMetadata: { cred_req: 'meta-data' },
metadata: { requestMetadata: { cred_req: 'meta-data' } },
state: CredentialState.RequestSent,
})
})
Expand Down Expand Up @@ -587,7 +591,7 @@ describe('CredentialService', () => {
requestMessage: new RequestCredentialMessage({
attachments: [requestAttachment],
}),
requestMetadata: { cred_req: 'meta-data' },
metadata: { requestMetadata: { cred_req: 'meta-data' } },
})

const credentialResponse = new IssueCredentialMessage({
Expand Down Expand Up @@ -684,6 +688,21 @@ describe('CredentialService', () => {
)
})

test('throws error when credential attribute values does not match received credential values', async () => {
repositoryFindByQueryMock.mockReturnValue(
Promise.resolve([
mockCredentialRecord({
state: CredentialState.RequestSent,
id: 'id',
// Take only first value from credential
credentialAttributes: [credentialPreview.attributes[0]],
}),
])
)

await expect(credentialService.processCredential(messageContext)).rejects.toThrowError()
})

const validState = CredentialState.RequestSent
const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState)
test(`throws an error when state transition is invalid`, async () => {
Expand All @@ -693,7 +712,7 @@ describe('CredentialService', () => {
Promise.resolve([
mockCredentialRecord({
state,
requestMetadata: { cred_req: 'meta-data' },
metadata: { requestMetadata: { cred_req: 'meta-data' } },
}),
])
)
Expand Down
Loading