Skip to content

Commit

Permalink
Add libs/auth tests, part 1 (#3075)
Browse files Browse the repository at this point in the history
* Add files to code coverage ignore list

* Add test-utils dependency

* Make minor tweaks to source files in prep for tests

E.g. export more, adjust logic to make achieving 100% code coverage
easier, etc.

* Add tests for apdu.ts

* Add tests for card_reader.ts

* Add tests for certs.ts

* Add tests for piv.ts

* Bump code coverage thresholds

* Update pnpm-lock.yaml

* Misc cleanup
  • Loading branch information
arsalansufi authored Mar 7, 2023
1 parent 9638c3f commit cd9f2b0
Show file tree
Hide file tree
Showing 13 changed files with 949 additions and 11 deletions.
15 changes: 11 additions & 4 deletions libs/auth/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ const shared = require('../../jest.config.shared');
*/
module.exports = {
...shared,
coveragePathIgnorePatterns: [
'src/index.ts',
'src/legacy/.*.ts',
'src/memory_card.ts',
'src/test_utils.ts',
'test/utils.ts,',
],
coverageThreshold: {
global: {
statements: 0,
branches: 0,
functions: 0,
lines: 0,
statements: 44,
branches: 31,
functions: 48,
lines: 44,
},
},
};
1 change: 1 addition & 0 deletions libs/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"@votingworks/test-utils": "workspace:*",
"esbuild-runner": "^2.2.1",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
Expand Down
235 changes: 235 additions & 0 deletions libs/auth/src/apdu.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { Buffer } from 'buffer';
import { Byte } from '@votingworks/types';

import { numericArray } from '../test/utils';
import {
CardCommand,
CommandApdu,
constructTlv,
parseTlv,
ResponseApduError,
} from './apdu';

test.each<{
cla?: { chained?: boolean; secure?: boolean };
expectedFirstByte: Byte;
}>([
{ cla: undefined, expectedFirstByte: 0x00 },
{ cla: {}, expectedFirstByte: 0x00 },
{ cla: { chained: true }, expectedFirstByte: 0x10 },
{ cla: { secure: true }, expectedFirstByte: 0x0c },
{ cla: { chained: true, secure: true }, expectedFirstByte: 0x1c },
])('CommandApdu CLA handling, $cla', ({ cla, expectedFirstByte }) => {
const apdu = new CommandApdu({
cla,
ins: 0x01,
p1: 0x02,
p2: 0x03,
});
expect(apdu.asBuffer()).toEqual(
Buffer.from([expectedFirstByte, 0x01, 0x02, 0x03, 0x00])
);
});

test('CommandApdu with data', () => {
const apdu = new CommandApdu({
ins: 0x01,
p1: 0x02,
p2: 0x03,
data: Buffer.from([0x04, 0x05]),
});
expect(apdu.asBuffer()).toEqual(
Buffer.from([0x00, 0x01, 0x02, 0x03, 0x02, 0x04, 0x05])
);
});

test('CommandApdu data length validation', () => {
expect(
() =>
new CommandApdu({
ins: 0x01,
p1: 0x02,
p2: 0x03,
data: Buffer.from(numericArray({ length: 256 })),
})
).toThrow('APDU data exceeds max command APDU data length');
});

test('CommandApdu as hex string', () => {
const apdu = new CommandApdu({
ins: 0xa1,
p1: 0xb2,
p2: 0xc3,
data: Buffer.from([0xd4, 0xe5]),
});
expect(apdu.asHexString()).toEqual('00a1b2c302d4e5');
expect(apdu.asHexString(':')).toEqual('00:a1:b2:c3:02:d4:e5');
});

test('CardCommand with no data', () => {
const command = new CardCommand({
ins: 0x01,
p1: 0x02,
p2: 0x03,
});
expect(command.asCommandApdus().map((apdu) => apdu.asBuffer())).toEqual([
Buffer.from([0x00, 0x01, 0x02, 0x03, 0x00]),
]);
});

test('CardCommand with data requiring a single APDU', () => {
const command = new CardCommand({
ins: 0x01,
p1: 0x02,
p2: 0x03,
data: Buffer.from(numericArray({ length: 255 })),
});
expect(command.asCommandApdus().map((apdu) => apdu.asBuffer())).toEqual([
Buffer.from([
0x00,
0x01,
0x02,
0x03,
0xff,
...numericArray({ length: 255 }),
]),
]);
});

test('CardCommand with data requiring multiple APDUs', () => {
const command = new CardCommand({
ins: 0x01,
p1: 0x02,
p2: 0x03,
data: Buffer.from([
...numericArray({ length: 200, value: 1 }),
...numericArray({ length: 200, value: 2 }),
...numericArray({ length: 200, value: 3 }),
]),
});
expect(command.asCommandApdus().map((apdu) => apdu.asBuffer())).toEqual([
Buffer.from([
0x10,
0x01,
0x02,
0x03,
0xff,
...numericArray({ length: 200, value: 1 }),
...numericArray({ length: 55, value: 2 }),
]),
Buffer.from([
0x10,
0x01,
0x02,
0x03,
0xff,
...numericArray({ length: 145, value: 2 }),
...numericArray({ length: 110, value: 3 }),
]),
Buffer.from([
0x00,
0x01,
0x02,
0x03,
0x5a, // 90 (600 - 255 - 255) in hex
...numericArray({ length: 90, value: 3 }),
]),
]);
});

test('constructTlv with Byte tag', () => {
const tlv = constructTlv(0x01, Buffer.from([0x02, 0x03]));
expect(tlv).toEqual(Buffer.from([0x01, 0x02, 0x02, 0x03]));
});

test('constructTlv with Buffer tag', () => {
const tlv = constructTlv(
Buffer.from([0x01, 0x02]),
Buffer.from([0x03, 0x04])
);
expect(tlv).toEqual(Buffer.from([0x01, 0x02, 0x02, 0x03, 0x04]));
});

test.each<{ valueLength: number; expectedTlvLength: Byte[] }>([
{ valueLength: 51, expectedTlvLength: [0x33] },
{ valueLength: 127, expectedTlvLength: [0x7f] },
{ valueLength: 147, expectedTlvLength: [0x81, 0x93] },
{ valueLength: 255, expectedTlvLength: [0x81, 0xff] },
{ valueLength: 3017, expectedTlvLength: [0x82, 0x0b, 0xc9] },
{ valueLength: 65535, expectedTlvLength: [0x82, 0xff, 0xff] },
])(
'constructTlv value length handling ($valueLength)',
({ valueLength, expectedTlvLength }) => {
const value = numericArray({ length: valueLength });
const tlv = constructTlv(0x01, Buffer.from(value));
expect(tlv).toEqual(Buffer.from([0x01, ...expectedTlvLength, ...value]));
}
);

test('constructTlv value length validation', () => {
expect(() =>
constructTlv(0x01, Buffer.from(numericArray({ length: 65536 })))
).toThrow('TLV value is too large');
});

test.each<{
tagAsByteOrBuffer: Byte | Buffer;
tlv: Buffer;
expectedOutput: [Buffer, Buffer, Buffer];
}>([
{
tagAsByteOrBuffer: 0x01,
tlv: Buffer.from([0x01, 0x7f, ...numericArray({ length: 127 })]),
expectedOutput: [
Buffer.from([0x01]),
Buffer.from([0x7f]),
Buffer.from(numericArray({ length: 127 })),
],
},
{
tagAsByteOrBuffer: 0x01,
tlv: Buffer.from([0x01, 0x81, 0xff, ...numericArray({ length: 255 })]),
expectedOutput: [
Buffer.from([0x01]),
Buffer.from([0x81, 0xff]),
Buffer.from(numericArray({ length: 255 })),
],
},
{
tagAsByteOrBuffer: 0x01,
tlv: Buffer.from([
0x01,
0x82,
0xff,
0xff,
...numericArray({ length: 65535 }),
]),
expectedOutput: [
Buffer.from([0x01]),
Buffer.from([0x82, 0xff, 0xff]),
Buffer.from(numericArray({ length: 65535 })),
],
},
{
tagAsByteOrBuffer: Buffer.from([0x01, 0x02]),
tlv: Buffer.from([0x01, 0x02, 0x01, 0x00]),
expectedOutput: [
Buffer.from([0x01, 0x02]),
Buffer.from([0x01]),
Buffer.from([0x00]),
],
},
])('parseTlv', ({ tagAsByteOrBuffer, tlv, expectedOutput }) => {
expect(parseTlv(tagAsByteOrBuffer, tlv)).toEqual(expectedOutput);
});

test('ResponseApduError', () => {
const error = new ResponseApduError([0x6a, 0x82]);
expect(error.message).toEqual(
'Received response APDU with non-success status: 6a 82'
);
expect(error.statusWord()).toEqual([0x6a, 0x82]);
expect(error.hasStatusWord([0x6a, 0x82])).toEqual(true);
expect(error.hasStatusWord([0x6b, 0x82])).toEqual(false);
expect(error.hasStatusWord([0x6a, 0x83])).toEqual(false);
});
14 changes: 11 additions & 3 deletions libs/auth/src/apdu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ export const MAX_APDU_LENGTH = 260;
* The max length of a command APDU's data. The `- 5` accounts for the CLA, INS, P1, P2, and Lc
* (see CommandApdu below).
*/
const MAX_COMMAND_APDU_DATA_LENGTH = MAX_APDU_LENGTH - 5;
export const MAX_COMMAND_APDU_DATA_LENGTH = MAX_APDU_LENGTH - 5;

/**
* The max length of a response APDU's data. The `- 2` accounts for the status word (see
* STATUS_WORD below).
*/
export const MAX_RESPONSE_APDU_DATA_LENGTH = MAX_APDU_LENGTH - 2;

/**
* Because APDUs have a max length, commands involving larger amounts of data have to be sent as
Expand Down Expand Up @@ -66,8 +72,10 @@ export const GET_RESPONSE = {
} as const;

function splitEvery2Characters(s: string): string[] {
assert(s.length % 2 === 0);
return s.match(/.{2}/g) || [];
assert(s.length > 0 && s.length % 2 === 0);
const sSplit = s.match(/.{2}/g);
assert(sSplit !== null);
return sSplit;
}

/**
Expand Down
Loading

0 comments on commit cd9f2b0

Please sign in to comment.