Skip to content

Commit

Permalink
feat: add credential info to access attributes (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimoGlastra authored May 6, 2021
1 parent e07b90e commit 2fef3aa
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 107 deletions.
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

0 comments on commit 2fef3aa

Please sign in to comment.