-
Notifications
You must be signed in to change notification settings - Fork 7
Add CVM (Cardholder Verification Method) parsing and evaluation #105
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
Conversation
- Add parseCvmList() to parse EMV tag 8E CVM List data - Add evaluateCvm() to select appropriate verification method based on context - Support all standard CVM conditions (terminal support, amount thresholds, etc.) - Export CvmMethod, CvmCondition, CvmRule, CvmList, CvmContext types - Add comprehensive tests for CVM parsing and evaluation Closes #104
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds CVM (Cardholder Verification Method) parsing and evaluation functionality to the EMV library. It introduces the ability to parse EMV tag 8E (CVM List) data and evaluate CVM rules against transaction contexts to select the appropriate verification method.
- Adds
parseCvmList()function to decode CVM List data structure with amount thresholds and verification rules - Adds
evaluateCvm()function to select appropriate verification methods based on transaction context - Exports five new types:
CvmMethod,CvmCondition,CvmRule,CvmList, andCvmContext
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/index.ts | Exports new CVM-related functions (parseCvmList, evaluateCvm) and types (CvmMethod, CvmCondition, CvmRule, CvmList, CvmContext) |
| src/emv-application.ts | Implements CVM parsing logic with bit manipulation for method codes and condition bytes, plus evaluation logic for 11 different condition types |
| src/emv-application.test.ts | Adds test suites for both parsing and evaluation functions covering basic scenarios, amount thresholds, and fail-on-unsuccessful flag handling |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| describe('evaluateCvm', async () => { | ||
| const { parseCvmList, evaluateCvm } = await import('./emv-application.js'); | ||
|
|
||
| it('should select first matching rule', () => { | ||
| const cvmList = parseCvmList(Buffer.from([ | ||
| 0x00, 0x00, 0x00, 0x00, | ||
| 0x00, 0x00, 0x00, 0x00, | ||
| 0x02, 0x03, // Enciphered PIN, if terminal supports | ||
| 0x1e, 0x03, // Signature, if terminal supports | ||
| ])); | ||
|
|
||
| const result = evaluateCvm(cvmList, { terminalSupportsCvm: true }); | ||
| assert.strictEqual(result?.method, 'enciphered_pin_online'); | ||
| }); | ||
|
|
||
| it('should skip rules where condition not met', () => { | ||
| const cvmList = parseCvmList(Buffer.from([ | ||
| 0x00, 0x00, 0x00, 0x00, | ||
| 0x00, 0x00, 0x00, 0x00, | ||
| 0x02, 0x03, // Enciphered PIN, if terminal supports | ||
| 0x1f, 0x00, // No CVM, always | ||
| ])); | ||
|
|
||
| const result = evaluateCvm(cvmList, { terminalSupportsCvm: false }); | ||
| assert.strictEqual(result?.method, 'no_cvm'); | ||
| }); | ||
|
|
||
| it('should handle amount threshold conditions', () => { | ||
| const cvmList = parseCvmList(Buffer.from([ | ||
| 0x00, 0x00, 0x03, 0xe8, // X = 1000 | ||
| 0x00, 0x00, 0x00, 0x00, // Y = 0 | ||
| 0x02, 0x07, // Enciphered PIN, if amount > X | ||
| 0x1f, 0x00, // No CVM, always | ||
| ])); | ||
|
|
||
| // Amount 500 is under X (1000), so PIN rule doesn't apply | ||
| const result1 = evaluateCvm(cvmList, { amount: 500 }); | ||
| assert.strictEqual(result1?.method, 'no_cvm'); | ||
|
|
||
| // Amount 1500 is over X, so PIN rule applies | ||
| const result2 = evaluateCvm(cvmList, { amount: 1500 }); | ||
| assert.strictEqual(result2?.method, 'enciphered_pin_online'); | ||
| }); | ||
|
|
||
| it('should return undefined if no rules match', () => { | ||
| const cvmList = parseCvmList(Buffer.from([ | ||
| 0x00, 0x00, 0x00, 0x00, | ||
| 0x00, 0x00, 0x00, 0x00, | ||
| 0x02, 0x03, // Enciphered PIN, if terminal supports | ||
| ])); | ||
|
|
||
| const result = evaluateCvm(cvmList, { terminalSupportsCvm: false }); | ||
| assert.strictEqual(result, undefined); | ||
| }); | ||
| }); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new CVM evaluation logic lacks test coverage for several condition types. While the implementation includes logic for conditions like 'unattended_cash', 'manual_cash', 'purchase_with_cashback', 'not_unattended_cash_manual_pin', and the 'amount_under_x/y' conditions, these are not tested. Consider adding test cases that verify these conditions work correctly, especially the complex boolean logic in 'not_unattended_cash_manual_pin'.
| describe('parseCvmList', async () => { | ||
| const { parseCvmList } = await import('./emv-application.js'); | ||
|
|
||
| it('should parse CVM list with amount thresholds', () => { | ||
| // CVM List: X=1000, Y=5000, then rules | ||
| const buffer = Buffer.from([ | ||
| 0x00, 0x00, 0x03, 0xe8, // X = 1000 | ||
| 0x00, 0x00, 0x13, 0x88, // Y = 5000 | ||
| 0x02, 0x03, // Enciphered PIN online, if terminal supports CVM | ||
| 0x1e, 0x03, // Signature, if terminal supports CVM | ||
| 0x1f, 0x00, // No CVM, always | ||
| ]); | ||
| const result = parseCvmList(buffer); | ||
|
|
||
| assert.strictEqual(result.amountX, 1000); | ||
| assert.strictEqual(result.amountY, 5000); | ||
| assert.strictEqual(result.rules.length, 3); | ||
|
|
||
| assert.strictEqual(result.rules[0]?.method, 'enciphered_pin_online'); | ||
| assert.strictEqual(result.rules[0]?.condition, 'terminal_supports_cvm'); | ||
| assert.strictEqual(result.rules[0]?.failIfUnsuccessful, true); | ||
|
|
||
| assert.strictEqual(result.rules[1]?.method, 'signature'); | ||
| assert.strictEqual(result.rules[2]?.method, 'no_cvm'); | ||
| }); | ||
|
|
||
| it('should handle continue-on-fail flag', () => { | ||
| const buffer = Buffer.from([ | ||
| 0x00, 0x00, 0x00, 0x00, // X = 0 | ||
| 0x00, 0x00, 0x00, 0x00, // Y = 0 | ||
| 0x42, 0x00, // Enciphered PIN online + continue if fails, always | ||
| ]); | ||
| const result = parseCvmList(buffer); | ||
|
|
||
| assert.strictEqual(result.rules[0]?.failIfUnsuccessful, false); | ||
| }); | ||
|
|
||
| it('should return empty rules for buffer too short', () => { | ||
| const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00]); | ||
| const result = parseCvmList(buffer); | ||
| assert.strictEqual(result.rules.length, 0); | ||
| }); | ||
| }); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test case for buffers with incomplete CVM rules (odd number of bytes after the 8-byte header). While the implementation correctly handles this by checking 'i + 1 < buffer.length', there's no test verifying this edge case. Consider adding a test with a 9-byte buffer to ensure incomplete rules are silently skipped.
Summary
parseCvmList()function to parse EMV tag 8E (CVM List) dataevaluateCvm()function to select the appropriate verification method based on transaction contextCvmMethod,CvmCondition,CvmRule,CvmList,CvmContextTest plan
parseCvmList()parsing various CVM list formatsevaluateCvm()with different contexts and conditionsCloses #104