Skip to content

Commit

Permalink
feat(mrtd): data parsing using @li0ard/tsemrtd (#41)
Browse files Browse the repository at this point in the history
* feat: add @li0ard/tsemrtd and esbuild to esm compatibility

* fix: update deps esm before jest

* fix: update deps esm before jest

* feat: testing

* fix: adjust esm modules

* fix: fix load params

* feat: add testing for data process

* fix: add testing for data process

* fix: add testing for data process

* fix: test

* docs: update in testing

* fix: parseEMrtdData

* feat: typed parse results

---------

Co-authored-by: Ariel Gentile <[email protected]>
  • Loading branch information
lotharking and genaris authored Oct 18, 2024
1 parent 246432a commit ab0fb73
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 13 deletions.
3 changes: 3 additions & 0 deletions jest.config.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const config: Config.InitialOptions = {
},
],
},
moduleNameMapper: {
'@li0ard/tsemrtd': '<rootDir>/src/esm/bundle.js',
},
}

export default config
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"prettier": "prettier '**/*.+(js|json|ts|tsx|md|yml|yaml)'",
"format": "yarn prettier --write",
"check-format": "yarn prettier --list-different",
"test": "jest",
"lint": "eslint --ignore-path .gitignore .",
"test": "yarn workspaces run test",
"lint": "eslint --ignore-path .gitignore --ignore-pattern '**/esm/' .",
"validate": "yarn lint && yarn check-types && yarn check-format"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/mrtd/docs/mrtd-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ didcomm.org/mrtd/1.0

- mrz-data-request
- mrz-data
- emrtd-data-request
- emrtd-data
7 changes: 5 additions & 2 deletions packages/mrtd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
"directory": "packages/mrtd"
},
"scripts": {
"build": "yarn run clean && yarn run compile",
"deps": "rm -f src/esm/bundle.js && esbuild --platform=node src/esm/index.ts --bundle --outfile=src/esm/bundle.js",
"build": "yarn run clean && yarn run compile && yarn run deps",
"clean": "rimraf -rf ./build",
"compile": "tsc -p tsconfig.build.json",
"prepublishOnly": "yarn run build",
"test": "jest"
"test": "yarn run deps && jest"
},
"devDependencies": {
"@credo-ts/askar": "^0.5.10",
Expand All @@ -35,8 +36,10 @@
"@credo-ts/core": "^0.5.10"
},
"dependencies": {
"@li0ard/tsemrtd": "^0.2.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"esbuild": "^0.24.0",
"mrz": "^4.2.0",
"tsyringe": "^4.8.0"
}
Expand Down
4 changes: 2 additions & 2 deletions packages/mrtd/src/DidCommMrtdEvents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { MrzData } from './models/MrzData'
import type { MrzData, EMrtdData } from './models'
import type { BaseEvent, ConnectionRecord } from '@credo-ts/core'

export enum MrtdEventTypes {
Expand Down Expand Up @@ -30,7 +30,7 @@ export interface EMrtdDataReceivedEvent extends BaseEvent {
type: MrtdEventTypes.EMrtdDataReceived
payload: {
connection: ConnectionRecord
dataGroups: Record<string, string>
dataGroups: EMrtdData
threadId: string
}
}
Expand Down
13 changes: 7 additions & 6 deletions packages/mrtd/src/DidCommMrtdService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MrzDataRequestedEvent,
} from './DidCommMrtdEvents'
import { EMrtdDataMessage, EMrtdDataRequestMessage, MrzDataMessage, MrzDataRequestMessage } from './messages'
import { parseEMrtdData } from './models'

@scoped(Lifecycle.ContainerScoped)
export class DidCommMrtdService {
Expand Down Expand Up @@ -77,22 +78,22 @@ export class DidCommMrtdService {
const connection = messageContext.assertReadyConnection()
const { agentContext, message } = messageContext

/*let parsed
let parsed
try {
const parseResult = Mrz.parse(message.mrzData)
const parseResult = parseEMrtdData(message.dataGroups)

parsed = { valid: parseResult.valid, fields: parseResult.fields, format: parseResult.format }
parsed = { valid: true, fields: parseResult }
} catch (error) {
// Unsupported format. Send raw data anyway
parsed = { valid: false, fields: {} }
}*/
parsed = { valid: false }
}

const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter)
eventEmitter.emit<EMrtdDataReceivedEvent>(agentContext, {
type: MrtdEventTypes.EMrtdDataReceived,
payload: {
connection,
dataGroups: message.dataGroups,
dataGroups: { raw: message.dataGroups, parsed },
threadId: message.threadId,
},
})
Expand Down
1 change: 1 addition & 0 deletions packages/mrtd/src/esm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bundle.js
3 changes: 3 additions & 0 deletions packages/mrtd/src/esm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { COM, DG1, DG2, DG3, DG4, DG5, DG7, DG11, DG12, DG14, DG15, SOD, PKD } from '@li0ard/tsemrtd'

export { COM, DG1, DG2, DG3, DG4, DG5, DG7, DG11, DG12, DG14, DG15, SOD, PKD }
100 changes: 100 additions & 0 deletions packages/mrtd/src/models/EMrtdData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { SecurityInfos } from '@li0ard/tsemrtd/dist/asn1/eac'
import type { CSCAMasterList } from '@li0ard/tsemrtd/dist/asn1/pkd'
import type {
DecodedAdditionalDocumentData,
DecodedAdditionalPersonalData,
DecodedCom,
DecodedFingerprint,
DecodedImage,
DecodedIris,
DecodedSecurtyObjectOfDocument,
} from '@li0ard/tsemrtd/dist/consts/interfaces'
import type { SubjectPublicKeyInfo } from '@peculiar/asn1-x509'

import { COM, DG1, DG2, DG3, DG4, DG5, DG7, DG11, DG12, DG14, DG15, SOD, PKD } from '../esm'

import { EMrtdDataGroup } from './EMrtdDataGroup'

export type EMrtdData = {
raw: Record<string, string>
parsed: { fields?: ParsedEMrtdData; valid: boolean }
}

export type ParsedEMrtdData = {
com: DecodedCom
mrzData: string
images: DecodedImage[]
fingerprints?: DecodedFingerprint[]
iris?: DecodedIris[]
displayedImages?: Buffer[]
signatureImages?: Buffer[]
additionalPersonalData?: DecodedAdditionalPersonalData
additionalDocumentData?: DecodedAdditionalDocumentData
securityInfos?: SecurityInfos
subjectPublicKeyInfo?: SubjectPublicKeyInfo
securityObjectOfDocument: DecodedSecurtyObjectOfDocument
cscaMasterList?: CSCAMasterList
}

/**
*
* @param input object containing base64-encoded eMRTD data groups
* @returns parsed eMRDT Data
* @throws Error in case of missing mandatory data (EF_COM, EF_DG1, EF_DG2 or EF_SOD)
*/
export function parseEMrtdData(input: Record<EMrtdDataGroup, string>): ParsedEMrtdData {
const parsedData: Partial<ParsedEMrtdData> = {}

for (const [key, value] of Object.entries(input)) {
const decodedValue = Buffer.from(value, 'base64')
switch (key as EMrtdDataGroup) {
case EMrtdDataGroup.COM:
parsedData.com = COM.load(decodedValue)
break
case EMrtdDataGroup.DG1:
parsedData.mrzData = DG1.load(decodedValue)
break
case EMrtdDataGroup.DG2:
parsedData.images = DG2.load(decodedValue)
break
case EMrtdDataGroup.DG3:
parsedData.fingerprints = DG3.load(decodedValue)
break
case EMrtdDataGroup.DG4:
parsedData.iris = DG4.load(decodedValue)
break
case EMrtdDataGroup.DG5:
parsedData.displayedImages = DG5.load(decodedValue)
break
case EMrtdDataGroup.DG7:
parsedData.signatureImages = DG7.load(decodedValue)
break
case EMrtdDataGroup.DG11:
parsedData.additionalPersonalData = DG11.load(decodedValue)
break
case EMrtdDataGroup.DG12:
parsedData.additionalDocumentData = DG12.load(decodedValue)
break
case EMrtdDataGroup.DG14:
parsedData.securityInfos = DG14.load(decodedValue)
break
case EMrtdDataGroup.DG15:
parsedData.subjectPublicKeyInfo = DG15.load(decodedValue)
break
case EMrtdDataGroup.SOD:
parsedData.securityObjectOfDocument = SOD.load(decodedValue)
break
case EMrtdDataGroup.PKD:
parsedData.cscaMasterList = PKD.load(decodedValue)
break
default:
break
}
}

if (!parsedData.com || !parsedData.mrzData || !parsedData.images || !parsedData.securityObjectOfDocument) {
throw Error('Parsed data misses mandatory files')
}

return parsedData as ParsedEMrtdData
}
15 changes: 15 additions & 0 deletions packages/mrtd/src/models/EMrtdDataGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum EMrtdDataGroup {
COM = 'COM',
DG1 = 'DG1',
DG2 = 'DG2',
DG3 = 'DG3',
DG4 = 'DG4',
DG5 = 'DG5',
DG7 = 'DG7',
DG11 = 'DG11',
DG12 = 'DG12',
DG14 = 'DG14',
DG15 = 'DG15',
SOD = 'SOD',
PKD = 'PKD',
}
2 changes: 2 additions & 0 deletions packages/mrtd/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './DidCommMrtdRole'
export * from './EMrtdData'
export * from './EMrtdDataGroup'
export * from './MrzData'
104 changes: 103 additions & 1 deletion packages/mrtd/tests/DidCommMrtdService.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import type { EMrtdDataReceivedEvent } from '../src/DidCommMrtdEvents'
import type { Agent } from '@credo-ts/core'

import { JsonTransformer } from '@credo-ts/core'
import {
AgentContext,
ConnectionRecord,
DidExchangeRole,
DidExchangeState,
EventEmitter,
InboundMessageContext,
JsonTransformer,
} from '@credo-ts/core'

import { MrtdEventTypes } from '../src/DidCommMrtdEvents'
import { DidCommMrtdService } from '../src/DidCommMrtdService'
import { EMrtdDataMessage } from '../src/messages'

import { setupAgent } from './utils/agent'

const passport = {} // For testing purposes, replace this value with a JSON file that contains the attributes: COM, DG1, DG2, DG11, and SOD.

const isPassportEmpty = !passport || (passport && Object.keys(passport).length === 0)

describe('Didcomm MRTD', () => {
let agent: Agent
let didcommMrtdService: DidCommMrtdService
let agentContext: AgentContext

beforeAll(async () => {
agent = await setupAgent({
name: 'mrtd service test',
})
didcommMrtdService = agent.dependencyManager.resolve(DidCommMrtdService)
agentContext = agent.dependencyManager.resolve(AgentContext)
})

afterAll(async () => {
Expand All @@ -41,4 +58,89 @@ describe('Didcomm MRTD', () => {
)
})
})

describe('EMRTD Data message', () => {
test('Should create a valid https://didcomm.org/mrtd/1.0/emrtd-data message ', async () => {
const message = didcommMrtdService.createEMrtdData({
threadId: '5678-5678-5678-5678',
dataGroups: passport,
})

const jsonMessage = JsonTransformer.toJSON(message)

expect(jsonMessage).toEqual(
expect.objectContaining({
'@id': expect.any(String),
'@type': 'https://didcomm.org/mrtd/1.0/emrtd-data',
dataGroups: isPassportEmpty
? {}
: {
COM: expect.any(String),
DG1: expect.any(String),
DG2: expect.any(String),
DG11: expect.any(String),
SOD: expect.any(String),
},
'~thread': expect.objectContaining({ thid: '5678-5678-5678-5678' }),
}),
)
})
})

describe('EMRTD Data process message', () => {
test('Should create a valid https://didcomm.org/mrtd/1.0/emrtd-data message ', async () => {
const emrtdDataMessage = new EMrtdDataMessage({
threadId: '5678-5678-5678-5678',
dataGroups: passport,
})

const mockConnectionRecord = new ConnectionRecord({
id: 'mockConnectionId',
state: DidExchangeState.Completed,
role: DidExchangeRole.Responder,
})

const inboundMessageContext = new InboundMessageContext<EMrtdDataMessage>(emrtdDataMessage, {
agentContext: agentContext,
connection: mockConnectionRecord,
})

const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter)
const event = new Promise((resolve) => {
eventEmitter.on(MrtdEventTypes.EMrtdDataReceived, (event: EMrtdDataReceivedEvent) => {
resolve(event)
})
})

await didcommMrtdService.processEMrtdData(inboundMessageContext)

const payload = ((await event) as EMrtdDataReceivedEvent).payload

const expectedRaw = expect.objectContaining({
COM: expect.any(String),
DG1: expect.any(String),
DG2: expect.any(String),
SOD: expect.any(String),
})

const expectedFields = expect.objectContaining({
com: expect.any(Object),
mrzData: expect.any(String),
images: expect.any(Array),
securityObjectOfDocument: expect.any(Object),
})

expect(payload).toEqual({
connection: mockConnectionRecord,
dataGroups: {
raw: isPassportEmpty ? {} : expectedRaw,
parsed: {
valid: isPassportEmpty ? false : true,
fields: isPassportEmpty ? undefined : expectedFields,
},
},
threadId: '5678-5678-5678-5678',
})
})
})
})
Loading

0 comments on commit ab0fb73

Please sign in to comment.