diff --git a/tests/meson.build b/tests/meson.build index 8dfebcd7..93124c33 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -322,136 +322,20 @@ foreach test_name: unit_tests ) endforeach -# Validity TLS unit tests (needs driver library + OpenSSL) +# Validity unit tests (needs driver library + OpenSSL) if 'validity' in supported_drivers openssl_dep = dependency('openssl', version: '>= 3.0', required: false) if openssl_dep.found() - validity_tls_test = executable('test-validity-tls', - sources: 'test-validity-tls.c', + validity_test = executable('test-validity', + sources: 'test-validity.c', dependencies: [ libfprint_private_dep, openssl_dep ], c_args: common_cflags, link_with: libfprint_drivers, + link_whole: test_utils, install: false, ) - test('validity-tls', - validity_tls_test, - suite: ['unit-tests'], - env: envs, - ) - endif - - # Validity fwext unit tests (no OpenSSL needed) - validity_fwext_test = executable('test-validity-fwext', - sources: 'test-validity-fwext.c', - dependencies: [ libfprint_private_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - install: false, - ) - test('validity-fwext', - validity_fwext_test, - suite: ['unit-tests'], - env: envs, - ) - - # Validity sensor identification unit tests (no OpenSSL needed) - validity_sensor_test = executable('test-validity-sensor', - sources: 'test-validity-sensor.c', - dependencies: [ libfprint_private_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - install: false, - ) - test('validity-sensor', - validity_sensor_test, - suite: ['unit-tests'], - env: envs, - ) - - # Validity capture infrastructure unit tests (needs OpenSSL for clean slate) - if openssl_dep.found() - validity_capture_test = executable('test-validity-capture', - sources: 'test-validity-capture.c', - dependencies: [ libfprint_private_dep, openssl_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - install: false, - ) - test('validity-capture', - validity_capture_test, - suite: ['unit-tests'], - env: envs, - ) - endif - - # Validity DB operations unit tests - validity_db_test = executable('test-validity-db', - sources: 'test-validity-db.c', - dependencies: [ libfprint_private_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - install: false, - ) - test('validity-db', - validity_db_test, - suite: ['unit-tests'], - env: envs, - ) - - # Validity verify/identify/delete/clear regression tests - validity_verify_test = executable('test-validity-verify', - sources: 'test-validity-verify.c', - dependencies: [ libfprint_private_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - link_whole: test_utils, - install: false, - ) - test('validity-verify', - validity_verify_test, - suite: ['unit-tests'], - env: envs, - ) - - # Validity enrollment response parsing unit tests - validity_enroll_test = executable('test-validity-enroll', - sources: 'test-validity-enroll.c', - dependencies: [ libfprint_private_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - install: false, - ) - test('validity-enroll', - validity_enroll_test, - suite: ['unit-tests'], - env: envs, - ) - - # Validity HAL unit tests - validity_hal_test = executable('test-validity-hal', - sources: 'test-validity-hal.c', - dependencies: [ libfprint_private_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - install: false, - ) - test('validity-hal', - validity_hal_test, - suite: ['unit-tests'], - env: envs, - ) - - # Validity pairing unit tests - if openssl_dep.found() - validity_pair_test = executable('test-validity-pair', - sources: 'test-validity-pair.c', - dependencies: [ libfprint_private_dep, openssl_dep ], - c_args: common_cflags, - link_with: libfprint_drivers, - install: false, - ) - test('validity-pair', - validity_pair_test, + test('validity', + validity_test, suite: ['unit-tests'], env: envs, ) diff --git a/tests/test-validity-capture.c b/tests/test-validity-capture.c deleted file mode 100644 index ef9b81c9..00000000 --- a/tests/test-validity-capture.c +++ /dev/null @@ -1,1024 +0,0 @@ -/* - * Unit tests for validity capture infrastructure - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include "fpi-byte-utils.h" - -#include "drivers/validity/validity_capture.h" -#include "drivers/validity/validity_sensor.h" - -/* ================================================================ - * T5.1: test_split_chunks_basic - * - * Verify that split_chunks correctly parses a TLV buffer with two - * known chunks and produces the right type, size, and data. - * ================================================================ */ -static void -test_split_chunks_basic (void) -{ - /* Build two TLV chunks: - * type=0x002a, size=4, data={0xAA,0xBB,0xCC,0xDD} - * type=0x0034, size=2, data={0x11,0x22} - */ - guint8 buf[] = { - 0x2a, 0x00, 0x04, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, - 0x34, 0x00, 0x02, 0x00, 0x11, 0x22, - }; - - gsize n = 0; - ValidityCaptureChunk *chunks; - - chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); - - g_assert_nonnull (chunks); - g_assert_cmpuint (n, ==, 2); - - g_assert_cmpuint (chunks[0].type, ==, 0x002a); - g_assert_cmpuint (chunks[0].size, ==, 4); - g_assert_cmpmem (chunks[0].data, 4, buf + 4, 4); - - g_assert_cmpuint (chunks[1].type, ==, 0x0034); - g_assert_cmpuint (chunks[1].size, ==, 2); - g_assert_cmpmem (chunks[1].data, 2, buf + 12, 2); - - validity_capture_chunks_free (chunks, n); -} - -/* ================================================================ - * T5.2: test_split_merge_roundtrip - * - * Verify that split then merge produces identical bytes. - * ================================================================ */ -static void -test_split_merge_roundtrip (void) -{ - guint8 buf[] = { - 0x2a, 0x00, 0x08, 0x00, - 0x20, 0x01, 0x01, 0x00, 0x10, 0x01, 0x00, 0x00, - 0x29, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x00, 0x00, - }; - - gsize n = 0; - ValidityCaptureChunk *chunks; - - chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); - g_assert_nonnull (chunks); - g_assert_cmpuint (n, ==, 2); - - gsize merged_len = 0; - guint8 *merged = validity_capture_merge_chunks (chunks, n, &merged_len); - - g_assert_nonnull (merged); - g_assert_cmpuint (merged_len, ==, sizeof (buf)); - g_assert_cmpmem (merged, merged_len, buf, sizeof (buf)); - - g_free (merged); - validity_capture_chunks_free (chunks, n); -} - -/* ================================================================ - * T5.3: test_split_chunks_empty - * - * Verify empty input returns empty result. - * ================================================================ */ -static void -test_split_chunks_empty (void) -{ - gsize n = 99; - ValidityCaptureChunk *chunks; - - chunks = validity_capture_split_chunks (NULL, 0, &n); - g_assert_null (chunks); - g_assert_cmpuint (n, ==, 0); -} - -/* ================================================================ - * T5.4: test_split_chunks_truncated - * - * Verify truncated chunk (size extends past end) returns NULL. - * ================================================================ */ -static void -test_split_chunks_truncated (void) -{ - /* type=0x0034, size=0x0008, but only 4 bytes of data follow */ - guint8 buf[] = { - 0x34, 0x00, 0x08, 0x00, 0x11, 0x22, 0x33, 0x44, - }; - - gsize n = 99; - ValidityCaptureChunk *chunks; - - chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); - g_assert_null (chunks); - g_assert_cmpuint (n, ==, 0); -} - -/* ================================================================ - * T5.5: test_decode_insn_noop - * - * Verify NOOP (0x00) decodes to opcode 0 with length 1. - * ================================================================ */ -static void -test_decode_insn_noop (void) -{ - guint8 data[] = { 0x00 }; - guint8 opcode, len, n_ops; - guint32 operands[3]; - - g_assert_true (validity_capture_decode_insn (data, 1, &opcode, &len, - operands, &n_ops)); - g_assert_cmpuint (opcode, ==, TST_OP_NOOP); - g_assert_cmpuint (len, ==, 1); - g_assert_cmpuint (n_ops, ==, 0); -} - -/* ================================================================ - * T5.6: test_decode_insn_call - * - * Verify Call instruction (0x10-0x17) decodes correctly with - * rx_inc, address, and repeat operands. - * ================================================================ */ -static void -test_decode_insn_call (void) -{ - /* Call: rx_inc=2, address=0x0a*4=0x28, repeat=8 */ - guint8 data[] = { 0x12, 0x0a, 0x08 }; - guint8 opcode, len, n_ops; - guint32 operands[3]; - - g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, - operands, &n_ops)); - g_assert_cmpuint (opcode, ==, TST_OP_CALL); - g_assert_cmpuint (len, ==, 3); - g_assert_cmpuint (n_ops, ==, 3); - g_assert_cmpuint (operands[0], ==, 2); /* rx_inc */ - g_assert_cmpuint (operands[1], ==, 0x28); /* address = 0x0a << 2 */ - g_assert_cmpuint (operands[2], ==, 8); /* repeat */ -} - -/* ================================================================ - * T5.7: test_decode_insn_call_repeat_zero - * - * Verify Call with repeat byte 0x00 decodes to repeat=0x100. - * ================================================================ */ -static void -test_decode_insn_call_repeat_zero (void) -{ - guint8 data[] = { 0x10, 0x05, 0x00 }; - guint8 opcode, len, n_ops; - guint32 operands[3]; - - g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, - operands, &n_ops)); - g_assert_cmpuint (opcode, ==, TST_OP_CALL); - g_assert_cmpuint (operands[2], ==, 0x100); -} - -/* ================================================================ - * T5.8: test_decode_insn_regwrite - * - * Verify Register Write (0x40-0x7f) decodes correctly: - * register address = (b0 & 0x3f) * 4 + 0x80002000 - * value = u16 LE from bytes 1-2 - * ================================================================ */ -static void -test_decode_insn_regwrite (void) -{ - /* b0=0x4f → reg = (0x0f)*4 + 0x80002000 = 0x8000203C, value=0x1234 */ - guint8 data[] = { 0x4f, 0x34, 0x12 }; - guint8 opcode, len, n_ops; - guint32 operands[3]; - - g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, - operands, &n_ops)); - g_assert_cmpuint (opcode, ==, TST_OP_REG_WRITE); - g_assert_cmpuint (len, ==, 3); - g_assert_cmpuint (n_ops, ==, 2); - g_assert_cmpuint (operands[0], ==, 0x8000203c); - g_assert_cmpuint (operands[1], ==, 0x1234); -} - -/* ================================================================ - * T5.9: test_decode_insn_enable_rx - * - * Verify Enable Rx (opcode 6) decodes as 2-byte instruction. - * ================================================================ */ -static void -test_decode_insn_enable_rx (void) -{ - guint8 data[] = { 0x06, 0x42 }; - guint8 opcode, len, n_ops; - guint32 operands[3]; - - g_assert_true (validity_capture_decode_insn (data, 2, &opcode, &len, - operands, &n_ops)); - g_assert_cmpuint (opcode, ==, TST_OP_ENABLE_RX); - g_assert_cmpuint (len, ==, 2); - g_assert_cmpuint (n_ops, ==, 1); - g_assert_cmpuint (operands[0], ==, 0x42); -} - -/* ================================================================ - * T5.10: test_decode_insn_sample - * - * Verify Sample (0x80-0xbf) decodes with two operands. - * ================================================================ */ -static void -test_decode_insn_sample (void) -{ - /* b0=0x8a → operand0 = (0x0a >> 3) & 7 = 1, operand1 = 0x0a & 7 = 2 */ - guint8 data[] = { 0x8a }; - guint8 opcode, len, n_ops; - guint32 operands[3]; - - g_assert_true (validity_capture_decode_insn (data, 1, &opcode, &len, - operands, &n_ops)); - g_assert_cmpuint (opcode, ==, TST_OP_SAMPLE); - g_assert_cmpuint (len, ==, 1); - g_assert_cmpuint (n_ops, ==, 2); - g_assert_cmpuint (operands[0], ==, 1); - g_assert_cmpuint (operands[1], ==, 2); -} - -/* ================================================================ - * T5.11: test_find_nth_insn - * - * Verify finding the Nth instruction of a given opcode in a buffer. - * ================================================================ */ -static void -test_find_nth_insn (void) -{ - /* Buffer: NOOP, NOOP, Call(rx=0,addr=0x14,rep=1), NOOP */ - guint8 data[] = { - 0x00, /* NOOP at offset 0 */ - 0x00, /* NOOP at offset 1 */ - 0x10, 0x05, 0x01, /* Call at offset 2 */ - 0x00, /* NOOP at offset 5 */ - }; - - /* 1st NOOP is at offset 0 */ - g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), - TST_OP_NOOP, 1), ==, 0); - /* 2nd NOOP is at offset 1 */ - g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), - TST_OP_NOOP, 2), ==, 1); - /* 3rd NOOP is at offset 5 */ - g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), - TST_OP_NOOP, 3), ==, 5); - /* 1st Call is at offset 2 */ - g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), - TST_OP_CALL, 1), ==, 2); - /* No 2nd Call */ - g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), - TST_OP_CALL, 2), ==, -1); -} - -/* ================================================================ - * T5.12: test_find_nth_regwrite - * - * Verify finding a Register Write to a specific register address. - * ================================================================ */ -static void -test_find_nth_regwrite (void) -{ - /* Buffer: RegWrite(0x80002000, 0x55), RegWrite(0x8000203C, 0xAB) */ - guint8 data[] = { - 0x40, 0x55, 0x00, /* reg = 0x80002000, val = 0x0055 */ - 0x4f, 0xAB, 0x00, /* reg = 0x8000203C, val = 0x00AB */ - }; - - /* Find 1st write to 0x8000203C → offset 3 */ - g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), - 0x8000203c, 1), ==, 3); - /* No 2nd write to 0x8000203C */ - g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), - 0x8000203c, 2), ==, -1); - /* Find 1st write to 0x80002000 → offset 0 */ - g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), - 0x80002000, 1), ==, 0); -} - -/* ================================================================ - * T5.13: test_patch_timeslot_table - * - * Verify that patch_timeslot_table multiplies Call repeat counts - * by the given multiplier. - * ================================================================ */ -static void -test_patch_timeslot_table (void) -{ - /* Call(rx=0, addr=0x14, repeat=3) followed by NOOP */ - guint8 data[] = { - 0x10, 0x05, 0x03, /* Call: repeat=3 */ - 0x00, /* NOOP */ - }; - - /* Multiply by 2, with inc_address=TRUE */ - g_assert_true (validity_capture_patch_timeslot_table (data, sizeof (data), - TRUE, 2)); - - /* repeat becomes 3*2=6 */ - g_assert_cmpuint (data[2], ==, 6); - /* address byte incremented */ - g_assert_cmpuint (data[1], ==, 6); -} - -/* ================================================================ - * T5.14: test_patch_timeslot_table_no_mult_for_repeat1 - * - * Verify that Call instructions with repeat <= 1 are NOT multiplied. - * ================================================================ */ -static void -test_patch_timeslot_table_no_mult_for_repeat1 (void) -{ - guint8 data[] = { - 0x10, 0x05, 0x01, /* Call: repeat=1 */ - 0x00, - }; - - g_assert_true (validity_capture_patch_timeslot_table (data, sizeof (data), - TRUE, 4)); - /* repeat stays 1 (not multiplied because <= 1) */ - g_assert_cmpuint (data[2], ==, 1); - /* address NOT incremented */ - g_assert_cmpuint (data[1], ==, 5); -} - -/* ================================================================ - * T5.15: test_bitpack_uniform - * - * When all values are identical, bitpack returns v0=0 (0 bits), - * v1=the common value, and zero-length packed data. - * ================================================================ */ -static void -test_bitpack_uniform (void) -{ - guint8 values[] = { 0x42, 0x42, 0x42, 0x42 }; - guint8 v0, v1; - gsize out_len; - - guint8 *packed = validity_capture_bitpack (values, 4, &v0, &v1, &out_len); - - g_assert_nonnull (packed); - g_assert_cmpuint (v0, ==, 0); - g_assert_cmpuint (v1, ==, 0x42); - g_assert_cmpuint (out_len, ==, 0); - - g_free (packed); -} - -/* ================================================================ - * T5.16: test_bitpack_range - * - * Verify bitpack with a small range of values. - * Values [10, 11, 12, 13] → delta range=3, useful_bits=2. - * ================================================================ */ -static void -test_bitpack_range (void) -{ - guint8 values[] = { 10, 11, 12, 13 }; - guint8 v0, v1; - gsize out_len; - - guint8 *packed = validity_capture_bitpack (values, 4, &v0, &v1, &out_len); - - g_assert_nonnull (packed); - g_assert_cmpuint (v0, ==, 2); /* 2 bits needed for max delta 3 */ - g_assert_cmpuint (v1, ==, 10); /* minimum value */ - - /* 4 values * 2 bits = 8 bits = 1 byte */ - g_assert_cmpuint (out_len, ==, 1); - - /* Deltas: [0, 1, 2, 3] - * Packed little-endian: bits 0-1 = 0b00, bits 2-3 = 0b01, - * bits 4-5 = 0b10, bits 6-7 = 0b11 - * Byte = 0b11100100 = 0xE4 */ - g_assert_cmpuint (packed[0], ==, 0xE4); - - g_free (packed); -} - -/* ================================================================ - * T5.17: test_factory_bits_parsing - * - * Verify parsing a synthetic factory bits response with subtag 3 - * (calibration values) and subtag 7 (calibration data). - * ================================================================ */ -static void -test_factory_bits_parsing (void) -{ - /* Factory bits response format: - * wtf(4LE) entries(4LE) - * entry: ptr(4LE) length(2LE) tag(2LE) subtag(2LE) flags(2LE) data[length] - */ - guint8 cal_values[] = { 0xAA, 0xBB, 0xCC, 0xDD }; - guint8 cal_data[] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }; - - /* Build response buffer */ - GByteArray *resp = g_byte_array_new (); - guint8 hdr[8]; - - /* Header: wtf=0, entries=2 */ - FP_WRITE_UINT32_LE (hdr, 0); - FP_WRITE_UINT32_LE (hdr + 4, 2); - g_byte_array_append (resp, hdr, 8); - - /* Entry 1: subtag=3, calibration values (4-byte header + actual data) */ - { - guint8 entry[12]; - guint16 length = 4 + sizeof (cal_values); /* 4-byte header + data */ - FP_WRITE_UINT32_LE (entry, 0); /* ptr */ - FP_WRITE_UINT16_LE (entry + 4, length); /* length */ - FP_WRITE_UINT16_LE (entry + 6, 0x0001); /* tag */ - FP_WRITE_UINT16_LE (entry + 8, 3); /* subtag = 3 */ - FP_WRITE_UINT16_LE (entry + 10, 0); /* flags */ - g_byte_array_append (resp, entry, 12); - - guint8 data_hdr[4] = { 0, 0, 0, 0 }; /* 4-byte header */ - g_byte_array_append (resp, data_hdr, 4); - g_byte_array_append (resp, cal_values, sizeof (cal_values)); - } - - /* Entry 2: subtag=7, calibration data (4-byte header + actual data) */ - { - guint8 entry[12]; - guint16 length = 4 + sizeof (cal_data); - FP_WRITE_UINT32_LE (entry, 0); - FP_WRITE_UINT16_LE (entry + 4, length); - FP_WRITE_UINT16_LE (entry + 6, 0x0002); - FP_WRITE_UINT16_LE (entry + 8, 7); /* subtag = 7 */ - FP_WRITE_UINT16_LE (entry + 10, 0); - g_byte_array_append (resp, entry, 12); - - guint8 data_hdr[4] = { 0, 0, 0, 0 }; - g_byte_array_append (resp, data_hdr, 4); - g_byte_array_append (resp, cal_data, sizeof (cal_data)); - } - - guint8 *out_cal_values = NULL, *out_cal_data = NULL; - gsize out_cal_values_len = 0, out_cal_data_len = 0; - - gboolean ok = validity_capture_parse_factory_bits ( - resp->data, resp->len, - &out_cal_values, &out_cal_values_len, - &out_cal_data, &out_cal_data_len); - - g_assert_true (ok); - g_assert_nonnull (out_cal_values); - g_assert_cmpuint (out_cal_values_len, ==, sizeof (cal_values)); - g_assert_cmpmem (out_cal_values, out_cal_values_len, - cal_values, sizeof (cal_values)); - - g_assert_nonnull (out_cal_data); - g_assert_cmpuint (out_cal_data_len, ==, sizeof (cal_data)); - g_assert_cmpmem (out_cal_data, out_cal_data_len, - cal_data, sizeof (cal_data)); - - g_free (out_cal_values); - g_free (out_cal_data); - g_byte_array_free (resp, TRUE); -} - -/* ================================================================ - * T5.18: test_factory_bits_no_subtag3 - * - * Verify that parsing fails when subtag 3 is missing. - * ================================================================ */ -static void -test_factory_bits_no_subtag3 (void) -{ - /* Build response with only subtag=7 (no subtag=3) */ - guint8 buf[32]; - - FP_WRITE_UINT32_LE (buf, 0); /* wtf */ - FP_WRITE_UINT32_LE (buf + 4, 1); /* entries=1 */ - - /* Entry: subtag=7, length=5 (4 hdr + 1 data) */ - FP_WRITE_UINT32_LE (buf + 8, 0); - FP_WRITE_UINT16_LE (buf + 12, 5); - FP_WRITE_UINT16_LE (buf + 14, 0x0001); - FP_WRITE_UINT16_LE (buf + 16, 7); /* subtag=7, not 3 */ - FP_WRITE_UINT16_LE (buf + 18, 0); - memset (buf + 20, 0, 5); - - guint8 *cv = NULL; - gsize cv_len = 0; - - gboolean ok = validity_capture_parse_factory_bits (buf, 25, - &cv, &cv_len, - NULL, NULL); - g_assert_false (ok); - g_assert_null (cv); -} - -/* ================================================================ - * T5.19: test_average_frames_interleave2 - * - * Verify frame averaging with interleave_lines=2 (repeat_multiplier=2). - * With 2 interleaved lines per calibration line, each output line - * should be the average of 2 input lines. - * ================================================================ */ -static void -test_average_frames_interleave2 (void) -{ - guint16 bytes_per_line = 4; - guint16 lines_per_calibration_data = 2; - guint16 lines_per_frame = 4; /* 2 cal lines * 2 interleave */ - guint8 calibration_frames = 1; - - /* Single frame: 4 lines * 4 bytes = 16 bytes */ - guint8 raw[] = { - 10, 20, 30, 40, /* line 0 (cal line 0, interleave 0) */ - 20, 30, 40, 50, /* line 1 (cal line 0, interleave 1) */ - 30, 40, 50, 60, /* line 2 (cal line 1, interleave 0) */ - 40, 50, 60, 70, /* line 3 (cal line 1, interleave 1) */ - }; - - gsize out_len = 0; - guint8 *result = validity_capture_average_frames ( - raw, sizeof (raw), - lines_per_frame, bytes_per_line, - lines_per_calibration_data, calibration_frames, - &out_len); - - g_assert_nonnull (result); - /* Output: 2 cal lines * 4 bytes = 8 bytes */ - g_assert_cmpuint (out_len, ==, 8); - - /* Cal line 0: avg of lines 0+1 → (10+20)/2=15, (20+30)/2=25, etc. */ - g_assert_cmpuint (result[0], ==, 15); - g_assert_cmpuint (result[1], ==, 25); - g_assert_cmpuint (result[2], ==, 35); - g_assert_cmpuint (result[3], ==, 45); - - /* Cal line 1: avg of lines 2+3 → (30+40)/2=35, (40+50)/2=45, etc. */ - g_assert_cmpuint (result[4], ==, 35); - g_assert_cmpuint (result[5], ==, 45); - g_assert_cmpuint (result[6], ==, 55); - g_assert_cmpuint (result[7], ==, 65); - - g_free (result); -} - -/* ================================================================ - * T5.20: test_clean_slate_roundtrip - * - * Verify that building a clean slate and then verifying it succeeds. - * ================================================================ */ -static void -test_clean_slate_roundtrip (void) -{ - guint8 test_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; - gsize slate_len = 0; - - guint8 *slate = validity_capture_build_clean_slate (test_data, - sizeof (test_data), - &slate_len); - - g_assert_nonnull (slate); - g_assert_cmpuint (slate_len, >, 68); - - /* Magic should be 0x5002 */ - g_assert_cmpuint (FP_READ_UINT16_LE (slate), ==, 0x5002); - - /* Verify should pass */ - g_assert_true (validity_capture_verify_clean_slate (slate, slate_len)); - - /* Corrupt one byte and verify should fail */ - slate[70] ^= 0xff; - g_assert_false (validity_capture_verify_clean_slate (slate, slate_len)); - - g_free (slate); -} - -/* ================================================================ - * T5.21: test_finger_mapping - * - * Verify all 10 finger mappings work in both directions. - * ================================================================ */ -static void -test_finger_mapping (void) -{ - /* FpFinger enum: LEFT_THUMB=1, ..., RIGHT_LITTLE=10 */ - for (guint f = 1; f <= 10; f++) - { - guint16 subtype = validity_finger_to_subtype (f); - g_assert_cmpuint (subtype, ==, f); - - gint back = validity_subtype_to_finger (subtype); - g_assert_cmpint (back, ==, (gint) f); - } - - /* Out of range */ - g_assert_cmpuint (validity_finger_to_subtype (0), ==, 0); - g_assert_cmpuint (validity_finger_to_subtype (11), ==, 0); - g_assert_cmpint (validity_subtype_to_finger (0), ==, -1); - g_assert_cmpint (validity_subtype_to_finger (11), ==, -1); -} - -/* ================================================================ - * T5.22: test_led_commands - * - * Verify LED start/end commands have correct format. - * ================================================================ */ -static void -test_led_commands (void) -{ - gsize start_len = 0, end_len = 0; - const guint8 *start_cmd = validity_capture_glow_start_cmd (&start_len); - const guint8 *end_cmd = validity_capture_glow_end_cmd (&end_len); - - g_assert_nonnull (start_cmd); - g_assert_nonnull (end_cmd); - - /* Both should be 125 bytes (LED control payload) */ - g_assert_cmpuint (start_len, ==, 125); - g_assert_cmpuint (end_len, ==, 125); - - /* Both should start with cmd byte 0x39 */ - g_assert_cmpuint (start_cmd[0], ==, 0x39); - g_assert_cmpuint (end_cmd[0], ==, 0x39); -} - -/* ================================================================ - * T5.23: test_capture_prog_lookup - * - * Verify that CaptureProg lookup returns data for known devices - * and NULL for unknown ones. - * ================================================================ */ -static void -test_capture_prog_lookup (void) -{ - gsize len = 0; - - /* Known: firmware 6.x, dev_type 0xb5 */ - const guint8 *prog = validity_capture_prog_lookup (6, 7, 0x00b5, &len); - - g_assert_nonnull (prog); - g_assert_cmpuint (len, >, 0); - - /* The program should be parseable as TLV chunks */ - gsize n_chunks = 0; - ValidityCaptureChunk *chunks = validity_capture_split_chunks (prog, len, &n_chunks); - g_assert_nonnull (chunks); - g_assert_cmpuint (n_chunks, >=, 4); /* At least ACM, CEM, TST, offset */ - - /* Check that we have the expected chunk types */ - gboolean has_acm = FALSE, has_tst = FALSE, has_2d = FALSE; - for (gsize i = 0; i < n_chunks; i++) - { - if (chunks[i].type == 0x002a) - has_acm = TRUE; - if (chunks[i].type == CAPT_CHUNK_TIMESLOT_2D) - has_tst = TRUE; - if (chunks[i].type == CAPT_CHUNK_2D_PARAMS) - has_2d = TRUE; - } - g_assert_true (has_acm); - g_assert_true (has_tst); - g_assert_true (has_2d); - - validity_capture_chunks_free (chunks, n_chunks); - - /* Also check 0x0885 (same geometry) */ - prog = validity_capture_prog_lookup (6, 0, 0x0885, &len); - g_assert_nonnull (prog); - - /* Unknown: firmware 5.x */ - prog = validity_capture_prog_lookup (5, 0, 0x00b5, &len); - g_assert_null (prog); - - /* Unknown: dev_type not in type1 list */ - prog = validity_capture_prog_lookup (6, 0, 0x1234, &len); - g_assert_null (prog); -} - -/* ================================================================ - * T5.24: test_capture_state_setup - * - * Verify that state setup correctly initializes all fields from - * sensor type info and factory bits. - * ================================================================ */ -static void -test_capture_state_setup (void) -{ - ValidityCaptureState state; - const ValiditySensorTypeInfo *type_info; - - type_info = validity_sensor_type_info_lookup (0x00b5); - g_assert_nonnull (type_info); - - /* Build minimal factory bits response with subtag 3 */ - guint8 cal_vals[] = { 0x10, 0x20, 0x30 }; - GByteArray *fb = g_byte_array_new (); - guint8 hdr[8]; - FP_WRITE_UINT32_LE (hdr, 0); - FP_WRITE_UINT32_LE (hdr + 4, 1); - g_byte_array_append (fb, hdr, 8); - - guint8 entry[12]; - guint16 length = 4 + sizeof (cal_vals); - FP_WRITE_UINT32_LE (entry, 0); - FP_WRITE_UINT16_LE (entry + 4, length); - FP_WRITE_UINT16_LE (entry + 6, 1); - FP_WRITE_UINT16_LE (entry + 8, 3); - FP_WRITE_UINT16_LE (entry + 10, 0); - g_byte_array_append (fb, entry, 12); - - guint8 data_hdr[4] = { 0 }; - g_byte_array_append (fb, data_hdr, 4); - g_byte_array_append (fb, cal_vals, sizeof (cal_vals)); - - validity_capture_state_init (&state); - gboolean ok = validity_capture_state_setup (&state, type_info, - 0x00b5, 6, 7, - fb->data, fb->len); - - g_assert_true (ok); - g_assert_true (state.is_type1_device); - g_assert_cmpuint (state.bytes_per_line, ==, 0x78); - g_assert_cmpuint (state.lines_per_frame, ==, 112 * 2); /* 224 */ - g_assert_cmpuint (state.key_calibration_line, ==, 56); /* 112/2 */ - g_assert_cmpuint (state.calibration_frames, ==, 3); - g_assert_cmpuint (state.calibration_iterations, ==, 3); - - g_assert_nonnull (state.factory_calibration_values); - g_assert_cmpuint (state.factory_calibration_values_len, ==, sizeof (cal_vals)); - g_assert_cmpmem (state.factory_calibration_values, - state.factory_calibration_values_len, - cal_vals, sizeof (cal_vals)); - - g_assert_nonnull (state.capture_prog); - g_assert_cmpuint (state.capture_prog_len, >, 0); - - validity_capture_state_clear (&state); - g_byte_array_free (fb, TRUE); -} - -/* ================================================================ - * T5.25: test_build_cmd_02_header - * - * Verify that build_cmd_02 produces the expected 5-byte header: - * cmd(0x02) | bytes_per_line(2LE) | req_lines(2LE) | chunks... - * ================================================================ */ -static void -test_build_cmd_02_header (void) -{ - ValidityCaptureState state; - const ValiditySensorTypeInfo *type_info; - - type_info = validity_sensor_type_info_lookup (0x00b5); - g_assert_nonnull (type_info); - - validity_capture_state_init (&state); - - /* Minimal setup: just enough for build_cmd_02 */ - gsize prog_len; - state.capture_prog = validity_capture_prog_lookup (6, 7, 0x00b5, &prog_len); - g_assert_nonnull (state.capture_prog); - state.capture_prog_len = prog_len; - state.is_type1_device = TRUE; - state.bytes_per_line = type_info->bytes_per_line; - state.lines_per_frame = 224; - state.calibration_frames = 3; - state.key_calibration_line = 56; - - /* Need factory calibration values (even if empty) for line_update */ - state.factory_calibration_values = g_malloc0 (112); - state.factory_calibration_values_len = 112; - - gsize cmd_len = 0; - guint8 *cmd = validity_capture_build_cmd_02 (&state, type_info, - VALIDITY_CAPTURE_CALIBRATE, - &cmd_len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (cmd_len, >=, 5); - - /* Byte 0: command = 0x02 */ - g_assert_cmpuint (cmd[0], ==, 0x02); - - /* Bytes 1-2: bytes_per_line = 0x0078 */ - g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 1), ==, 0x0078); - - /* Bytes 3-4: req_lines for CALIBRATE = frames * lines_per_frame + 1 */ - guint16 expected_lines = 3 * 224 + 1; - g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 3), ==, expected_lines); - - /* Remainder should be parseable as TLV chunks */ - gsize n_chunks = 0; - ValidityCaptureChunk *chunks = validity_capture_split_chunks ( - cmd + 5, cmd_len - 5, &n_chunks); - g_assert_nonnull (chunks); - g_assert_cmpuint (n_chunks, >=, 4); - - validity_capture_chunks_free (chunks, n_chunks); - g_free (cmd); - - /* Test IDENTIFY mode: req_lines should be 0 */ - cmd = validity_capture_build_cmd_02 (&state, type_info, - VALIDITY_CAPTURE_IDENTIFY, - &cmd_len); - g_assert_nonnull (cmd); - g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 3), ==, 0); - g_free (cmd); - - g_free (state.factory_calibration_values); -} - -/* ================================================================ - * T5.26: test_calibration_processing - * - * Verify that process_calibration applies scale and accumulates. - * ================================================================ */ -static void -test_calibration_processing (void) -{ - guint16 bytes_per_line = 16; - /* Single line with 8-byte header + 8 bytes of data */ - guint8 frame[16] = { - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* header (untouched) */ - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, /* data to scale */ - }; - - guint8 *calib = NULL; - gsize calib_len = 0; - - /* First call: initializes calib_data */ - validity_capture_process_calibration (&calib, &calib_len, - frame, sizeof (frame), - bytes_per_line); - - g_assert_nonnull (calib); - g_assert_cmpuint (calib_len, ==, 16); - - /* Header bytes should be preserved */ - g_assert_cmpuint (calib[0], ==, 0x00); - g_assert_cmpuint (calib[7], ==, 0x07); - - /* Data bytes at 0x80: scale(0x80) = (0x80 - 0x80) * 10 / 0x22 = 0 - * So all data bytes should be 0x00 */ - for (int i = 8; i < 16; i++) - g_assert_cmpuint (calib[i], ==, 0x00); - - /* Second call with same frame: accumulate */ - validity_capture_process_calibration (&calib, &calib_len, - frame, sizeof (frame), - bytes_per_line); - - /* add(0, 0) = 0, so data bytes still 0 */ - for (int i = 8; i < 16; i++) - g_assert_cmpuint (calib[i], ==, 0x00); - - g_free (calib); -} - -/* ================================================================ - * T5.27: test_capture_split_real_prog - * - * Parse the actual capture program for 0xb5 and verify - * expected chunks are present. - * ================================================================ */ -static void -test_capture_split_real_prog (void) -{ - gsize prog_len = 0; - const guint8 *prog = validity_capture_prog_lookup (6, 7, 0x00b5, &prog_len); - - g_assert_nonnull (prog); - - gsize n = 0; - ValidityCaptureChunk *chunks = validity_capture_split_chunks (prog, prog_len, &n); - - g_assert_nonnull (chunks); - g_assert_cmpuint (n, ==, 6); - - /* Expected order: 0x2a, 0x2c, 0x34, 0x2f, 0x29, 0x35 */ - g_assert_cmpuint (chunks[0].type, ==, 0x002a); - g_assert_cmpuint (chunks[0].size, ==, 8); - - g_assert_cmpuint (chunks[1].type, ==, 0x002c); - g_assert_cmpuint (chunks[1].size, ==, 40); - - g_assert_cmpuint (chunks[2].type, ==, CAPT_CHUNK_TIMESLOT_2D); - g_assert_cmpuint (chunks[2].size, ==, 64); - - g_assert_cmpuint (chunks[3].type, ==, CAPT_CHUNK_2D_PARAMS); - g_assert_cmpuint (chunks[3].size, ==, 4); - /* 2D value should be 112 (0x70) */ - g_assert_cmpuint (FP_READ_UINT32_LE (chunks[3].data), ==, 112); - - g_assert_cmpuint (chunks[4].type, ==, 0x0029); - g_assert_cmpuint (chunks[4].size, ==, 4); - - g_assert_cmpuint (chunks[5].type, ==, 0x0035); - g_assert_cmpuint (chunks[5].size, ==, 4); - - validity_capture_chunks_free (chunks, n); -} - -/* ================================================================ - * main - * ================================================================ */ -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - /* Chunk parsing */ - g_test_add_func ("/validity/capture/split-chunks-basic", - test_split_chunks_basic); - g_test_add_func ("/validity/capture/split-merge-roundtrip", - test_split_merge_roundtrip); - g_test_add_func ("/validity/capture/split-chunks-empty", - test_split_chunks_empty); - g_test_add_func ("/validity/capture/split-chunks-truncated", - test_split_chunks_truncated); - - /* Timeslot instruction decoder */ - g_test_add_func ("/validity/capture/decode-insn-noop", - test_decode_insn_noop); - g_test_add_func ("/validity/capture/decode-insn-call", - test_decode_insn_call); - g_test_add_func ("/validity/capture/decode-insn-call-repeat-zero", - test_decode_insn_call_repeat_zero); - g_test_add_func ("/validity/capture/decode-insn-regwrite", - test_decode_insn_regwrite); - g_test_add_func ("/validity/capture/decode-insn-enable-rx", - test_decode_insn_enable_rx); - g_test_add_func ("/validity/capture/decode-insn-sample", - test_decode_insn_sample); - - /* Instruction search */ - g_test_add_func ("/validity/capture/find-nth-insn", - test_find_nth_insn); - g_test_add_func ("/validity/capture/find-nth-regwrite", - test_find_nth_regwrite); - - /* Timeslot patching */ - g_test_add_func ("/validity/capture/patch-timeslot-table", - test_patch_timeslot_table); - g_test_add_func ("/validity/capture/patch-timeslot-no-mult-repeat1", - test_patch_timeslot_table_no_mult_for_repeat1); - - /* Bitpack */ - g_test_add_func ("/validity/capture/bitpack-uniform", - test_bitpack_uniform); - g_test_add_func ("/validity/capture/bitpack-range", - test_bitpack_range); - - /* Factory bits */ - g_test_add_func ("/validity/capture/factory-bits-parsing", - test_factory_bits_parsing); - g_test_add_func ("/validity/capture/factory-bits-no-subtag3", - test_factory_bits_no_subtag3); - - /* Frame averaging */ - g_test_add_func ("/validity/capture/average-frames-interleave2", - test_average_frames_interleave2); - - /* Clean slate */ - g_test_add_func ("/validity/capture/clean-slate-roundtrip", - test_clean_slate_roundtrip); - - /* Finger mapping */ - g_test_add_func ("/validity/capture/finger-mapping", - test_finger_mapping); - - /* LED commands */ - g_test_add_func ("/validity/capture/led-commands", - test_led_commands); - - /* CaptureProg lookup */ - g_test_add_func ("/validity/capture/prog-lookup", - test_capture_prog_lookup); - - /* State setup */ - g_test_add_func ("/validity/capture/state-setup", - test_capture_state_setup); - - /* build_cmd_02 */ - g_test_add_func ("/validity/capture/build-cmd-02-header", - test_build_cmd_02_header); - - /* Calibration processing */ - g_test_add_func ("/validity/capture/calibration-processing", - test_calibration_processing); - - /* Real capture program parsing */ - g_test_add_func ("/validity/capture/split-real-prog", - test_capture_split_real_prog); - - return g_test_run (); -} diff --git a/tests/test-validity-db.c b/tests/test-validity-db.c deleted file mode 100644 index 6d040c7d..00000000 --- a/tests/test-validity-db.c +++ /dev/null @@ -1,764 +0,0 @@ -/* - * Unit tests for validity database operations - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include "fpi-byte-utils.h" - -#include "drivers/validity/validity_db.h" -#include "drivers/validity/validity.h" -#include "drivers/validity/vcsfw_protocol.h" - -/* ================================================================ - * T6.1: test_cmd_db_info - * - * Verify cmd 0x45 (DB info) is a single-byte command. - * ================================================================ */ -static void -test_cmd_db_info (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_info (&len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 1); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DB_INFO); -} - -/* ================================================================ - * T6.2: test_cmd_get_user_storage - * - * Verify cmd 0x4B format with a known storage name. - * ================================================================ */ -static void -test_cmd_get_user_storage (void) -{ - gsize len; - const gchar *name = "StgWindsor"; - gsize name_len = strlen (name) + 1; /* includes NUL */ - g_autofree guint8 *cmd = validity_db_build_cmd_get_user_storage (name, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 1 + 2 + 2 + name_len); /* cmd + dbid + name_len + name */ - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER_STORAGE); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0); /* dbid = 0 → lookup by name */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, name_len); - g_assert_cmpmem (&cmd[5], name_len, name, name_len); -} - -/* ================================================================ - * T6.3: test_cmd_get_user_storage_null_name - * - * When name is NULL, should produce a command with zero name_len. - * ================================================================ */ -static void -test_cmd_get_user_storage_null_name (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_get_user_storage (NULL, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 5); /* cmd(1) + dbid(2) + name_len(2) */ - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER_STORAGE); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); /* name_len = 0 */ -} - -/* ================================================================ - * T6.4: test_cmd_get_user - * - * Verify cmd 0x4A format for get-by-dbid. - * ================================================================ */ -static void -test_cmd_get_user (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_get_user (0x1234, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 7); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x1234); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); -} - -/* ================================================================ - * T6.5: test_cmd_lookup_user - * - * Verify cmd 0x4A format for lookup-by-identity. - * ================================================================ */ -static void -test_cmd_lookup_user (void) -{ - gsize len; - guint8 identity[] = { 0x01, 0x02, 0x03, 0x04 }; - g_autofree guint8 *cmd = validity_db_build_cmd_lookup_user ( - 0x0003, identity, sizeof (identity), &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 7 + sizeof (identity)); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0); /* dbid = 0 */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0x0003); /* storage */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, sizeof (identity)); - g_assert_cmpmem (&cmd[7], sizeof (identity), identity, sizeof (identity)); -} - -/* ================================================================ - * T6.6: test_cmd_new_record - * - * Verify cmd 0x47 format. - * ================================================================ */ -static void -test_cmd_new_record (void) -{ - gsize len; - guint8 data[] = { 0xAA, 0xBB }; - g_autofree guint8 *cmd = validity_db_build_cmd_new_record ( - 0x0003, 0x0005, 0x0003, data, sizeof (data), &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 9 + sizeof (data)); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_NEW_RECORD); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x0003); /* parent */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0x0005); /* type */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0x0003); /* storage */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, sizeof (data)); - g_assert_cmpmem (&cmd[9], sizeof (data), data, sizeof (data)); -} - -/* ================================================================ - * T6.7: test_cmd_del_record - * - * Verify cmd 0x48 format. - * ================================================================ */ -static void -test_cmd_del_record (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_del_record (0xABCD, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 3); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DEL_RECORD); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0xABCD); -} - -/* ================================================================ - * T6.8: test_cmd_create_enrollment - * - * Verify cmd 0x69 start and end variants. - * ================================================================ */ -static void -test_cmd_create_enrollment (void) -{ - gsize len; - - /* Start enrollment */ - g_autofree guint8 *cmd_start = validity_db_build_cmd_create_enrollment (TRUE, &len); - - g_assert_nonnull (cmd_start); - g_assert_cmpuint (len, ==, 5); - g_assert_cmpuint (cmd_start[0], ==, VCSFW_CMD_CREATE_ENROLLMENT); - g_assert_cmpuint (FP_READ_UINT32_LE (&cmd_start[1]), ==, 1); - - /* End enrollment */ - g_autofree guint8 *cmd_end = validity_db_build_cmd_create_enrollment (FALSE, &len); - g_assert_nonnull (cmd_end); - g_assert_cmpuint (len, ==, 5); - g_assert_cmpuint (cmd_end[0], ==, VCSFW_CMD_CREATE_ENROLLMENT); - g_assert_cmpuint (FP_READ_UINT32_LE (&cmd_end[1]), ==, 0); -} - -/* ================================================================ - * T6.9: test_cmd_enrollment_update_start - * - * Verify cmd 0x68 format. - * ================================================================ */ -static void -test_cmd_enrollment_update_start (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_enrollment_update_start (0x12345678, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 9); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_ENROLLMENT_UPDATE_START); - g_assert_cmpuint (FP_READ_UINT32_LE (&cmd[1]), ==, 0x12345678); - g_assert_cmpuint (FP_READ_UINT32_LE (&cmd[5]), ==, 0); -} - -/* ================================================================ - * T6.10: test_cmd_match_finger - * - * Verify cmd 0x5E format (13 bytes per python-validity). - * ================================================================ */ -static void -test_cmd_match_finger (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_match_finger (&len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 13); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_FINGER); - g_assert_cmpuint (cmd[1], ==, 0x02); - g_assert_cmpuint (cmd[2], ==, 0xFF); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, 1); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[9]), ==, 0); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[11]), ==, 0); -} - -/* ================================================================ - * T6.11: test_cmd_get_match_result - * - * Verify cmd 0x60 format. - * ================================================================ */ -static void -test_cmd_get_match_result (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_get_match_result (&len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 5); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_MATCH_RESULT); - /* remaining bytes should be 0 */ - g_assert_cmpuint (cmd[1], ==, 0); - g_assert_cmpuint (cmd[2], ==, 0); - g_assert_cmpuint (cmd[3], ==, 0); - g_assert_cmpuint (cmd[4], ==, 0); -} - -/* ================================================================ - * T6.12: test_cmd_match_cleanup - * - * Verify cmd 0x62 format. - * ================================================================ */ -static void -test_cmd_match_cleanup (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_match_cleanup (&len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 5); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_CLEANUP); -} - -/* ================================================================ - * T6.13: test_cmd_get_prg_status - * - * Verify cmd 0x51 both normal and extended variant. - * ================================================================ */ -static void -test_cmd_get_prg_status (void) -{ - gsize len; - - g_autofree guint8 *normal = validity_db_build_cmd_get_prg_status (FALSE, &len); - - g_assert_cmpuint (len, ==, 5); - g_assert_cmpuint (normal[0], ==, VCSFW_CMD_GET_PRG_STATUS); - /* Normal: 00000000 */ - g_assert_cmpuint (normal[1], ==, 0); - g_assert_cmpuint (normal[2], ==, 0); - g_assert_cmpuint (normal[3], ==, 0); - g_assert_cmpuint (normal[4], ==, 0); - - g_autofree guint8 *ext = validity_db_build_cmd_get_prg_status (TRUE, &len); - g_assert_cmpuint (len, ==, 5); - g_assert_cmpuint (ext[0], ==, VCSFW_CMD_GET_PRG_STATUS); - /* Extended: 00200000 LE */ - g_assert_cmpuint (ext[1], ==, 0x00); - g_assert_cmpuint (ext[2], ==, 0x20); - g_assert_cmpuint (ext[3], ==, 0x00); - g_assert_cmpuint (ext[4], ==, 0x00); -} - -/* ================================================================ - * T6.14: test_cmd_capture_stop - * - * Verify cmd 0x04 format. - * ================================================================ */ -static void -test_cmd_capture_stop (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_capture_stop (&len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 1); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_CAPTURE_STOP); -} - -/* ================================================================ - * T6.15: test_cmd_call_cleanups - * - * Verify cmd 0x1a format. - * ================================================================ */ -static void -test_cmd_call_cleanups (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_call_cleanups (&len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 1); - g_assert_cmpuint (cmd[0], ==, 0x1a); -} - -/* ================================================================ - * T6.16: test_parse_db_info - * - * Construct a known db_info binary response and verify parsing. - * ================================================================ */ -static void -test_parse_db_info (void) -{ - guint8 data[0x1C]; /* 24 bytes header + 4 bytes for 2 roots */ - ValidityDbInfo info; - - memset (data, 0, sizeof (data)); - - /* Header: unknown1=1, unknown0=0, total=65536, used=1024, free=64512, records=10, n_roots=2 */ - FP_WRITE_UINT32_LE (&data[0], 1); /* unknown1 */ - FP_WRITE_UINT32_LE (&data[4], 0); /* unknown0 */ - FP_WRITE_UINT32_LE (&data[8], 65536); /* total */ - FP_WRITE_UINT32_LE (&data[12], 1024); /* used */ - FP_WRITE_UINT32_LE (&data[16], 64512); /* free */ - FP_WRITE_UINT16_LE (&data[20], 10); /* records */ - FP_WRITE_UINT16_LE (&data[22], 2); /* n_roots */ - FP_WRITE_UINT16_LE (&data[24], 0x0001); /* root[0] */ - FP_WRITE_UINT16_LE (&data[26], 0x0003); /* root[1] */ - - g_assert_true (validity_db_parse_info (data, sizeof (data), &info)); - - g_assert_cmpuint (info.unknown1, ==, 1); - g_assert_cmpuint (info.unknown0, ==, 0); - g_assert_cmpuint (info.total, ==, 65536); - g_assert_cmpuint (info.used, ==, 1024); - g_assert_cmpuint (info.free_space, ==, 64512); - g_assert_cmpuint (info.records, ==, 10); - g_assert_cmpuint (info.n_roots, ==, 2); - g_assert_nonnull (info.roots); - g_assert_cmpuint (info.roots[0], ==, 1); - g_assert_cmpuint (info.roots[1], ==, 3); - - validity_db_info_clear (&info); -} - -/* ================================================================ - * T6.17: test_parse_db_info_too_short - * - * A response shorter than 24 bytes should fail. - * ================================================================ */ -static void -test_parse_db_info_too_short (void) -{ - guint8 data[20] = { 0 }; - ValidityDbInfo info; - - g_assert_false (validity_db_parse_info (data, sizeof (data), &info)); -} - -/* ================================================================ - * T6.18: test_parse_user_storage - * - * Construct a user storage response with 2 users and verify. - * ================================================================ */ -static void -test_parse_user_storage (void) -{ - /* Header: dbid=3, user_count=2, name_sz=11, unknown=0 - * User table: {dbid=10, val_sz=100}, {dbid=11, val_sz=200} - * Name: "StgWindsor\0" */ - gsize name_len = strlen ("StgWindsor") + 1; - gsize total = 8 + 2 * 4 + name_len; - g_autofree guint8 *data = g_new0 (guint8, total); - - FP_WRITE_UINT16_LE (&data[0], 3); /* dbid */ - FP_WRITE_UINT16_LE (&data[2], 2); /* user_count */ - FP_WRITE_UINT16_LE (&data[4], name_len); /* name_sz */ - FP_WRITE_UINT16_LE (&data[6], 0); /* unknown */ - - FP_WRITE_UINT16_LE (&data[8], 10); /* user[0].dbid */ - FP_WRITE_UINT16_LE (&data[10], 100); /* user[0].val_sz */ - FP_WRITE_UINT16_LE (&data[12], 11); /* user[1].dbid */ - FP_WRITE_UINT16_LE (&data[14], 200); /* user[1].val_sz */ - - memcpy (&data[16], "StgWindsor", name_len); - - ValidityUserStorage storage; - g_assert_true (validity_db_parse_user_storage (data, total, &storage)); - - g_assert_cmpuint (storage.dbid, ==, 3); - g_assert_cmpuint (storage.user_count, ==, 2); - g_assert_cmpstr (storage.name, ==, "StgWindsor"); - g_assert_nonnull (storage.user_dbids); - g_assert_cmpuint (storage.user_dbids[0], ==, 10); - g_assert_cmpuint (storage.user_dbids[1], ==, 11); - g_assert_cmpuint (storage.user_val_sizes[0], ==, 100); - g_assert_cmpuint (storage.user_val_sizes[1], ==, 200); - - validity_user_storage_clear (&storage); -} - -/* ================================================================ - * T6.19: test_parse_user - * - * Construct a user response with one finger and verify. - * ================================================================ */ -static void -test_parse_user (void) -{ - guint8 identity_bytes[] = { 0xDE, 0xAD, 0xBE, 0xEF }; - /* Header: dbid=10, finger_count=1, unknown=0, identity_sz=4 - * Finger: dbid=20, subtype=2, storage=3, value_size=500 - * Identity: 4 bytes */ - gsize total = 8 + 8 + sizeof (identity_bytes); - g_autofree guint8 *data = g_new0 (guint8, total); - - FP_WRITE_UINT16_LE (&data[0], 10); /* dbid */ - FP_WRITE_UINT16_LE (&data[2], 1); /* finger_count */ - FP_WRITE_UINT16_LE (&data[4], 0); /* unknown */ - FP_WRITE_UINT16_LE (&data[6], sizeof (identity_bytes)); /* identity_sz */ - - /* Finger entry */ - FP_WRITE_UINT16_LE (&data[8], 20); /* finger.dbid */ - FP_WRITE_UINT16_LE (&data[10], 2); /* finger.subtype = right index */ - FP_WRITE_UINT16_LE (&data[12], 3); /* finger.storage */ - FP_WRITE_UINT16_LE (&data[14], 500); /* finger.value_size */ - - memcpy (&data[16], identity_bytes, sizeof (identity_bytes)); - - ValidityUser user; - g_assert_true (validity_db_parse_user (data, total, &user)); - - g_assert_cmpuint (user.dbid, ==, 10); - g_assert_cmpuint (user.finger_count, ==, 1); - g_assert_nonnull (user.fingers); - g_assert_cmpuint (user.fingers[0].dbid, ==, 20); - g_assert_cmpuint (user.fingers[0].subtype, ==, 2); - g_assert_cmpuint (user.fingers[0].storage, ==, 3); - g_assert_cmpuint (user.fingers[0].value_size, ==, 500); - g_assert_nonnull (user.identity); - g_assert_cmpuint (user.identity_len, ==, sizeof (identity_bytes)); - g_assert_cmpmem (user.identity, user.identity_len, - identity_bytes, sizeof (identity_bytes)); - - validity_user_clear (&user); -} - -/* ================================================================ - * T6.20: test_parse_new_record_id - * - * Verify parsing of new_record response (cmd 0x47). - * ================================================================ */ -static void -test_parse_new_record_id (void) -{ - guint16 record_id; - guint8 data[] = { 0x42, 0x00 }; - - g_assert_true (validity_db_parse_new_record_id (data, sizeof (data), &record_id)); - g_assert_cmpuint (record_id, ==, 0x0042); -} - -/* ================================================================ - * T6.21: test_parse_new_record_id_too_short - * - * A 1-byte response should fail. - * ================================================================ */ -static void -test_parse_new_record_id_too_short (void) -{ - guint16 record_id; - guint8 data[] = { 0x42 }; - - g_assert_false (validity_db_parse_new_record_id (data, sizeof (data), &record_id)); -} - -/* ================================================================ - * T6.22: test_build_identity - * - * Build a UUID identity and verify structure. - * ================================================================ */ -static void -test_build_identity (void) -{ - gsize len; - const gchar *uuid = "550e8400-e29b-41d4-a716-446655440000"; - g_autofree guint8 *id = validity_db_build_identity (uuid, &len); - - g_assert_nonnull (id); - /* Minimum size enforced */ - g_assert_cmpuint (len, >=, VALIDITY_IDENTITY_MIN_SIZE); - - /* type = SID (3) */ - g_assert_cmpuint (FP_READ_UINT32_LE (&id[0]), ==, VALIDITY_IDENTITY_TYPE_SID); - - /* len field = UUID string length */ - gsize uuid_len = strlen (uuid); - g_assert_cmpuint (FP_READ_UINT32_LE (&id[4]), ==, uuid_len); - - /* UUID payload */ - g_assert_cmpmem (&id[8], uuid_len, uuid, uuid_len); - - /* Remaining bytes should be zero-padded */ - for (gsize i = 8 + uuid_len; i < len; i++) - g_assert_cmpuint (id[i], ==, 0); -} - -/* ================================================================ - * T6.23: test_build_finger_data - * - * Build finger data and verify the tagged format. - * ================================================================ */ -static void -test_build_finger_data (void) -{ - gsize len; - guint8 template[] = { 0x11, 0x22, 0x33 }; - guint8 tid[] = { 0xAA, 0xBB }; - g_autofree guint8 *fd = validity_db_build_finger_data ( - 2, template, sizeof (template), tid, sizeof (tid), &len); - - g_assert_nonnull (fd); - - /* Check header: subtype(2) | flags=3(2) | tinfo_len(2) | 0x20(2) */ - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[0]), ==, 2); /* subtype */ - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[2]), ==, 3); /* flags */ - - gsize expected_tinfo_len = 4 + sizeof (template) + 4 + sizeof (tid); - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[4]), ==, expected_tinfo_len); - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[6]), ==, 0x20); - - /* Tag 1 (template) at offset 8 */ - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[8]), ==, 1); - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[10]), ==, sizeof (template)); - g_assert_cmpmem (&fd[12], sizeof (template), template, sizeof (template)); - - /* Tag 2 (tid) at offset 12+3 = 15 */ - gsize tid_offset = 12 + sizeof (template); - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[tid_offset]), ==, 2); - g_assert_cmpuint (FP_READ_UINT16_LE (&fd[tid_offset + 2]), ==, sizeof (tid)); - g_assert_cmpmem (&fd[tid_offset + 4], sizeof (tid), tid, sizeof (tid)); - - /* Total should be header(8) + tinfo + 0x20 padding */ - gsize expected_total = 8 + expected_tinfo_len + 0x20; - g_assert_cmpuint (len, ==, expected_total); -} - -/* ================================================================ - * T6.24: test_db_write_enable_blob - * - * Verify db_write_enable blob accessor returns a valid blob. - * ================================================================ */ -static void -test_db_write_enable_blob (void) -{ - gsize len; - - /* Test with 009a device type (known to have a 3621-byte blob) */ - const guint8 *blob = validity_db_get_write_enable_blob (VALIDITY_DEV_9A, &len); - - g_assert_nonnull (blob); - g_assert_cmpuint (len, >, 0); - g_assert_cmpuint (len, ==, 3621); - - /* Test all supported device types return valid blobs */ - const guint dev_types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, - VALIDITY_DEV_9A, VALIDITY_DEV_9D }; - for (guint i = 0; i < G_N_ELEMENTS (dev_types); i++) - { - gsize dbe_len; - const guint8 *dbe = validity_db_get_write_enable_blob (dev_types[i], &dbe_len); - g_assert_nonnull (dbe); - g_assert_cmpuint (dbe_len, >, 0); - } -} - -/* ================================================================ - * T6.25: test_parse_record_value - * - * Construct a record value response and verify parsing. - * ================================================================ */ -static void -test_parse_record_value (void) -{ - guint8 value[] = { 0x01, 0x02, 0x03 }; - /* Format: dbid(2) type(2) storage(2) sz(2) pad(2) value */ - gsize total = 10 + sizeof (value); - g_autofree guint8 *data = g_new0 (guint8, total); - - FP_WRITE_UINT16_LE (&data[0], 42); /* dbid */ - FP_WRITE_UINT16_LE (&data[2], 8); /* type = DATA */ - FP_WRITE_UINT16_LE (&data[4], 3); /* storage */ - FP_WRITE_UINT16_LE (&data[6], sizeof (value)); /* sz */ - FP_WRITE_UINT16_LE (&data[8], 0); /* pad */ - memcpy (&data[10], value, sizeof (value)); - - ValidityDbRecord record; - g_assert_true (validity_db_parse_record_value (data, total, &record)); - - g_assert_cmpuint (record.dbid, ==, 42); - g_assert_cmpuint (record.type, ==, 8); - g_assert_cmpuint (record.storage, ==, 3); - g_assert_nonnull (record.value); - g_assert_cmpuint (record.value_len, ==, sizeof (value)); - g_assert_cmpmem (record.value, record.value_len, value, sizeof (value)); - - validity_db_record_clear (&record); -} - -/* ================================================================ - * T6.26: test_parse_record_children - * - * Construct a record children response and verify parsing. - * ================================================================ */ -static void -test_parse_record_children (void) -{ - /* Format: dbid(2) type(2) storage(2) sz(2) cnt(2) pad(2) - * children[cnt * 4: dbid(2) type(2)] */ - gsize total = 12 + 2 * 4; - g_autofree guint8 *data = g_new0 (guint8, total); - - FP_WRITE_UINT16_LE (&data[0], 3); /* dbid */ - FP_WRITE_UINT16_LE (&data[2], 4); /* type = STORAGE */ - FP_WRITE_UINT16_LE (&data[4], 3); /* storage */ - FP_WRITE_UINT16_LE (&data[6], 0); /* sz */ - FP_WRITE_UINT16_LE (&data[8], 2); /* child_count */ - FP_WRITE_UINT16_LE (&data[10], 0); /* pad */ - - /* Children */ - FP_WRITE_UINT16_LE (&data[12], 10); /* child[0].dbid */ - FP_WRITE_UINT16_LE (&data[14], 5); /* child[0].type = USER */ - FP_WRITE_UINT16_LE (&data[16], 11); /* child[1].dbid */ - FP_WRITE_UINT16_LE (&data[18], 5); /* child[1].type = USER */ - - ValidityRecordChildren children; - g_assert_true (validity_db_parse_record_children (data, total, &children)); - - g_assert_cmpuint (children.dbid, ==, 3); - g_assert_cmpuint (children.type, ==, 4); - g_assert_cmpuint (children.storage, ==, 3); - g_assert_cmpuint (children.child_count, ==, 2); - g_assert_nonnull (children.children); - g_assert_cmpuint (children.children[0].dbid, ==, 10); - g_assert_cmpuint (children.children[0].type, ==, 5); - g_assert_cmpuint (children.children[1].dbid, ==, 11); - g_assert_cmpuint (children.children[1].type, ==, 5); - - validity_record_children_clear (&children); -} - -/* ================================================================ - * T6.27: test_cmd_enrollment_update - * - * Verify cmd 0x6B format with template data. - * ================================================================ */ -static void -test_cmd_enrollment_update (void) -{ - gsize len; - guint8 prev[] = { 0xDE, 0xAD, 0xBE, 0xEF }; - g_autofree guint8 *cmd = validity_db_build_cmd_enrollment_update ( - prev, sizeof (prev), &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 1 + sizeof (prev)); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_ENROLLMENT_UPDATE); - g_assert_cmpmem (&cmd[1], sizeof (prev), prev, sizeof (prev)); -} - -/* ================================================================ - * T6.28: test_cmd_get_record_value - * - * Verify cmd 0x49 format. - * ================================================================ */ -static void -test_cmd_get_record_value (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_get_record_value (0x5678, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 3); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_RECORD_VALUE); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x5678); -} - -/* ================================================================ - * T6.29: test_cmd_get_record_children - * - * Verify cmd 0x46 format. - * ================================================================ */ -static void -test_cmd_get_record_children (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_get_record_children (0x1234, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 3); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_RECORD_CHILDREN); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x1234); -} - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - /* Command builder tests */ - g_test_add_func ("/validity/db/cmd_db_info", test_cmd_db_info); - g_test_add_func ("/validity/db/cmd_get_user_storage", test_cmd_get_user_storage); - g_test_add_func ("/validity/db/cmd_get_user_storage_null_name", test_cmd_get_user_storage_null_name); - g_test_add_func ("/validity/db/cmd_get_user", test_cmd_get_user); - g_test_add_func ("/validity/db/cmd_lookup_user", test_cmd_lookup_user); - g_test_add_func ("/validity/db/cmd_new_record", test_cmd_new_record); - g_test_add_func ("/validity/db/cmd_del_record", test_cmd_del_record); - g_test_add_func ("/validity/db/cmd_create_enrollment", test_cmd_create_enrollment); - g_test_add_func ("/validity/db/cmd_enrollment_update_start", test_cmd_enrollment_update_start); - g_test_add_func ("/validity/db/cmd_enrollment_update", test_cmd_enrollment_update); - g_test_add_func ("/validity/db/cmd_match_finger", test_cmd_match_finger); - g_test_add_func ("/validity/db/cmd_get_match_result", test_cmd_get_match_result); - g_test_add_func ("/validity/db/cmd_match_cleanup", test_cmd_match_cleanup); - g_test_add_func ("/validity/db/cmd_get_prg_status", test_cmd_get_prg_status); - g_test_add_func ("/validity/db/cmd_capture_stop", test_cmd_capture_stop); - g_test_add_func ("/validity/db/cmd_call_cleanups", test_cmd_call_cleanups); - g_test_add_func ("/validity/db/cmd_get_record_value", test_cmd_get_record_value); - g_test_add_func ("/validity/db/cmd_get_record_children", test_cmd_get_record_children); - - /* Response parser tests */ - g_test_add_func ("/validity/db/parse_info", test_parse_db_info); - g_test_add_func ("/validity/db/parse_info_too_short", test_parse_db_info_too_short); - g_test_add_func ("/validity/db/parse_user_storage", test_parse_user_storage); - g_test_add_func ("/validity/db/parse_user", test_parse_user); - g_test_add_func ("/validity/db/parse_new_record_id", test_parse_new_record_id); - g_test_add_func ("/validity/db/parse_new_record_id_too_short", test_parse_new_record_id_too_short); - g_test_add_func ("/validity/db/parse_record_value", test_parse_record_value); - g_test_add_func ("/validity/db/parse_record_children", test_parse_record_children); - - /* Identity and finger data tests */ - g_test_add_func ("/validity/db/build_identity", test_build_identity); - g_test_add_func ("/validity/db/build_finger_data", test_build_finger_data); - - /* Blob accessor test */ - g_test_add_func ("/validity/db/write_enable_blob", test_db_write_enable_blob); - - return g_test_run (); -} diff --git a/tests/test-validity-enroll.c b/tests/test-validity-enroll.c deleted file mode 100644 index 6ed3d9d9..00000000 --- a/tests/test-validity-enroll.c +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Unit tests for enrollment response parsing - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include "fpi-byte-utils.h" -#include "drivers/validity/validity.h" - -/* ================================================================ - * Helper: build a tagged block - * [tag:2LE][len:2LE][padding:MAGIC_LEN][payload:len] - * Total block size = 4 + MAGIC_LEN + len = MAGIC_LEN + len + 4 - * Wait — re-read the parser: - * tag(2LE) | len(2LE) => block_size = MAGIC_LEN + len - * so the full block is [tag:2][len:2] + body[MAGIC_LEN + len] - * No — looking at the code: pos + 4 reads tag+len, then - * block_size = MAGIC_LEN + len, and the block starts at data[pos]. - * Template: data[pos .. pos + block_size]. - * Header: data[pos + MAGIC_LEN .. pos + MAGIC_LEN + len]. - * Advance: pos += block_size. - * - * Actually re-reading more carefully: - * tag = data[pos], len = data[pos+2] - * block_size = MAGIC_LEN + len - * template = data[pos .. pos + block_size] - * So the 4 bytes of tag+len are INSIDE the block_size. - * MAGIC_LEN = 0x38 = 56 which is > 4, so tag+len fit inside. - * - * To build test data: write tag(2LE) at offset 0, len(2LE) at - * offset 2, then (MAGIC_LEN - 4) padding bytes, then len payload bytes. - * Total = MAGIC_LEN + len. - * ================================================================ */ -static guint8 * -build_block (guint16 tag, const guint8 *payload, guint16 payload_len, - gsize *out_len) -{ - gsize block_size = ENROLLMENT_MAGIC_LEN + payload_len; - guint8 *buf = g_malloc0 (block_size); - - FP_WRITE_UINT16_LE (buf, tag); - FP_WRITE_UINT16_LE (buf + 2, payload_len); - - if (payload && payload_len > 0) - memcpy (buf + ENROLLMENT_MAGIC_LEN, payload, payload_len); - - *out_len = block_size; - return buf; -} - -/* Wrap raw block data with the 2-byte declared_len prefix the parser expects: - * [declared_len:2LE][blocks...] - * declared_len = blocks_len (total size of all concatenated blocks). */ -static guint8 * -wrap_response (const guint8 *blocks, gsize blocks_len, gsize *out_len) -{ - *out_len = 2 + blocks_len; - - guint8 *buf = g_malloc (*out_len); - - FP_WRITE_UINT16_LE (buf, (guint16) blocks_len); - if (blocks && blocks_len > 0) - memcpy (buf + 2, blocks, blocks_len); - return buf; -} - -/* ================================================================ - * T8.1: parse empty data — returns TRUE, all fields NULL - * ================================================================ */ -static void -test_parse_empty (void) -{ - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (NULL, 0, &result); - - /* Empty data (len < 2) → parser returns FALSE */ - g_assert_false (ok); -} - -/* ================================================================ - * T8.2: parse single template block (tag=0) - * ================================================================ */ -static void -test_parse_template_block (void) -{ - guint8 payload[] = { 0xDE, 0xAD, 0xBE, 0xEF }; - gsize block_len; - g_autofree guint8 *block = build_block (0, payload, sizeof (payload), - &block_len); - gsize resp_len; - g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); - - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (data, resp_len, &result); - - g_assert_true (ok); - g_assert_nonnull (result.template_data); - g_assert_cmpuint (result.template_len, ==, block_len); - g_assert_null (result.header); - g_assert_null (result.tid); - - enrollment_update_result_clear (&result); -} - -/* ================================================================ - * T8.3: parse header block (tag=1) - * ================================================================ */ -static void -test_parse_header_block (void) -{ - guint8 payload[] = { 0x01, 0x02, 0x03 }; - gsize block_len; - g_autofree guint8 *block = build_block (1, payload, sizeof (payload), - &block_len); - gsize resp_len; - g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); - - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (data, resp_len, &result); - - g_assert_true (ok); - g_assert_nonnull (result.header); - g_assert_cmpuint (result.header_len, ==, sizeof (payload)); - g_assert_cmpmem (result.header, result.header_len, payload, sizeof (payload)); - g_assert_null (result.template_data); - g_assert_null (result.tid); - - enrollment_update_result_clear (&result); -} - -/* ================================================================ - * T8.4: parse tid block (tag=3) — signals enrollment complete - * ================================================================ */ -static void -test_parse_tid_block (void) -{ - guint8 payload[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; - gsize block_len; - g_autofree guint8 *block = build_block (3, payload, sizeof (payload), - &block_len); - gsize resp_len; - g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); - - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (data, resp_len, &result); - - g_assert_true (ok); - g_assert_nonnull (result.tid); - g_assert_cmpuint (result.tid_len, ==, sizeof (payload)); - g_assert_cmpmem (result.tid, result.tid_len, payload, sizeof (payload)); - g_assert_null (result.template_data); - g_assert_null (result.header); - - enrollment_update_result_clear (&result); -} - -/* ================================================================ - * T8.5: parse multiple blocks — template + header + tid - * ================================================================ */ -static void -test_parse_multiple_blocks (void) -{ - guint8 tmpl_payload[] = { 0x11, 0x22 }; - guint8 hdr_payload[] = { 0x33, 0x44, 0x55 }; - guint8 tid_payload[] = { 0x66 }; - - gsize tmpl_len, hdr_len, tid_len; - g_autofree guint8 *tmpl = build_block (0, tmpl_payload, - sizeof (tmpl_payload), &tmpl_len); - g_autofree guint8 *hdr = build_block (1, hdr_payload, - sizeof (hdr_payload), &hdr_len); - g_autofree guint8 *tid = build_block (3, tid_payload, - sizeof (tid_payload), &tid_len); - - /* Concatenate all three blocks, then wrap with length prefix */ - gsize blocks_total = tmpl_len + hdr_len + tid_len; - g_autofree guint8 *blocks = g_malloc (blocks_total); - - memcpy (blocks, tmpl, tmpl_len); - memcpy (blocks + tmpl_len, hdr, hdr_len); - memcpy (blocks + tmpl_len + hdr_len, tid, tid_len); - - gsize resp_len; - g_autofree guint8 *data = wrap_response (blocks, blocks_total, &resp_len); - - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (data, resp_len, &result); - - g_assert_true (ok); - g_assert_nonnull (result.template_data); - g_assert_nonnull (result.header); - g_assert_nonnull (result.tid); - g_assert_cmpuint (result.template_len, ==, tmpl_len); - g_assert_cmpuint (result.header_len, ==, sizeof (hdr_payload)); - g_assert_cmpuint (result.tid_len, ==, sizeof (tid_payload)); - - enrollment_update_result_clear (&result); -} - -/* ================================================================ - * T8.6: parse truncated data — stops before reading past buffer - * ================================================================ */ -static void -test_parse_truncated (void) -{ - /* Build a response where the declared length is consistent with data_len - * but the block content is too short for a full block to be parsed. - * declared_len = 6, so data = [06 00][tag:2][len:2][2 more bytes] - * The block_size = MAGIC_LEN + len will exceed 8 for any len > 0, - * so the parser's "pos + block_size > data_len" check will skip it. */ - guint8 data[8]; - - FP_WRITE_UINT16_LE (data, 6); /* declared_len = 6 */ - FP_WRITE_UINT16_LE (data + 2, 0); /* tag = 0 (template) */ - FP_WRITE_UINT16_LE (data + 4, 10); /* len = 10 → block_size = MAGIC_LEN + 10 > 8 */ - data[6] = 0; - data[7] = 0; - - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (data, sizeof (data), &result); - - g_assert_true (ok); - /* No fields should be populated since the block was truncated */ - g_assert_null (result.template_data); - g_assert_null (result.header); - g_assert_null (result.tid); -} - -/* ================================================================ - * T8.7: parse unknown tag — silently skipped - * ================================================================ */ -static void -test_parse_unknown_tag (void) -{ - guint8 payload[] = { 0x99 }; - gsize block_len; - g_autofree guint8 *block = build_block (42, payload, sizeof (payload), - &block_len); - gsize resp_len; - g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); - - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (data, resp_len, &result); - - g_assert_true (ok); - g_assert_null (result.template_data); - g_assert_null (result.header); - g_assert_null (result.tid); -} - -/* ================================================================ - * T8.8: result_clear — frees and zeroes - * ================================================================ */ -static void -test_result_clear (void) -{ - EnrollmentUpdateResult result; - - result.header = g_malloc (10); - result.header_len = 10; - result.template_data = g_malloc (20); - result.template_len = 20; - result.tid = g_malloc (5); - result.tid_len = 5; - - enrollment_update_result_clear (&result); - - g_assert_null (result.header); - g_assert_null (result.template_data); - g_assert_null (result.tid); - g_assert_cmpuint (result.header_len, ==, 0); - g_assert_cmpuint (result.template_len, ==, 0); - g_assert_cmpuint (result.tid_len, ==, 0); -} - -/* ================================================================ - * T8.9: parse zero-length payload — tag present but no data - * ================================================================ */ -static void -test_parse_zero_length_payload (void) -{ - gsize block_len; - g_autofree guint8 *block = build_block (1, NULL, 0, &block_len); - gsize resp_len; - g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); - - EnrollmentUpdateResult result; - gboolean ok = parse_enrollment_update_response (data, resp_len, &result); - - g_assert_true (ok); - /* Tag 1 with len=0: header should be NULL (len > 0 check in parser) */ - g_assert_null (result.header); -} - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/validity/enroll/parse-empty", - test_parse_empty); - g_test_add_func ("/validity/enroll/parse-template-block", - test_parse_template_block); - g_test_add_func ("/validity/enroll/parse-header-block", - test_parse_header_block); - g_test_add_func ("/validity/enroll/parse-tid-block", - test_parse_tid_block); - g_test_add_func ("/validity/enroll/parse-multiple-blocks", - test_parse_multiple_blocks); - g_test_add_func ("/validity/enroll/parse-truncated", - test_parse_truncated); - g_test_add_func ("/validity/enroll/parse-unknown-tag", - test_parse_unknown_tag); - g_test_add_func ("/validity/enroll/result-clear", - test_result_clear); - g_test_add_func ("/validity/enroll/parse-zero-length-payload", - test_parse_zero_length_payload); - - return g_test_run (); -} diff --git a/tests/test-validity-fwext.c b/tests/test-validity-fwext.c deleted file mode 100644 index 5a19262b..00000000 --- a/tests/test-validity-fwext.c +++ /dev/null @@ -1,644 +0,0 @@ -/* - * Unit tests for validity firmware extension upload functions - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include -#include - -#include "fpi-device.h" -#include "fpi-ssm.h" -#include "fpi-byte-utils.h" - -#include "drivers/validity/validity_fwext.h" -#include "drivers/validity/vcsfw_protocol.h" - -/* ================================================================ - * T3.1: test_fw_info_parse_present - * - * Verify that a valid GET_FW_INFO response (status=OK) with 1 module - * is parsed correctly into ValidityFwInfo. - * ================================================================ */ -static void -test_fw_info_parse_present (void) -{ - ValidityFwInfo info; - - /* Build a synthetic GET_FW_INFO response: - * major(2) + minor(2) + modcnt(2) + buildtime(4) = 10 header bytes - * + 1 module * 12 bytes = 12 - * Total = 22 bytes (data after 2-byte status, which is stripped) */ - guint8 data[22]; - - memset (data, 0, sizeof (data)); - - /* major = 6 */ - data[0] = 6; - data[1] = 0; - /* minor = 7 */ - data[2] = 7; - data[3] = 0; - /* module_count = 1 */ - data[4] = 1; - data[5] = 0; - /* buildtime = 0x12345678 LE */ - data[6] = 0x78; - data[7] = 0x56; - data[8] = 0x34; - data[9] = 0x12; - /* Module 0: type=3, subtype=4, major=1, minor=2, size=0x1000 */ - data[10] = 3; - data[11] = 0; - data[12] = 4; - data[13] = 0; - data[14] = 1; - data[15] = 0; - data[16] = 2; - data[17] = 0; - data[18] = 0x00; - data[19] = 0x10; - data[20] = 0x00; - data[21] = 0x00; - - gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data), - VCSFW_STATUS_OK, &info); - - g_assert_true (ok); - g_assert_true (info.loaded); - g_assert_cmpuint (info.major, ==, 6); - g_assert_cmpuint (info.minor, ==, 7); - g_assert_cmpuint (info.module_count, ==, 1); - g_assert_cmpuint (info.buildtime, ==, 0x12345678); - g_assert_cmpuint (info.modules[0].type, ==, 3); - g_assert_cmpuint (info.modules[0].subtype, ==, 4); - g_assert_cmpuint (info.modules[0].major, ==, 1); - g_assert_cmpuint (info.modules[0].minor, ==, 2); - g_assert_cmpuint (info.modules[0].size, ==, 0x1000); -} - -/* ================================================================ - * T3.2: test_fw_info_parse_absent - * - * Verify that status VCSFW_STATUS_NO_FW sets loaded=FALSE. - * ================================================================ */ -static void -test_fw_info_parse_absent (void) -{ - ValidityFwInfo info; - - gboolean ok = validity_fwext_parse_fw_info (NULL, 0, - VCSFW_STATUS_NO_FW, &info); - - g_assert_true (ok); - g_assert_false (info.loaded); -} - -/* ================================================================ - * T3.2b: test_fw_info_parse_unknown_status - * - * Verify that an unexpected status returns FALSE. - * ================================================================ */ -static void -test_fw_info_parse_unknown_status (void) -{ - ValidityFwInfo info; - - g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING, - "*unexpected status*"); - - gboolean ok = validity_fwext_parse_fw_info (NULL, 0, 0x9999, &info); - - g_test_assert_expected_messages (); - - g_assert_false (ok); - g_assert_false (info.loaded); -} - -/* ================================================================ - * T3.2c: test_fw_info_parse_truncated - * - * Verify that status=OK but data too short returns FALSE. - * ================================================================ */ -static void -test_fw_info_parse_truncated (void) -{ - ValidityFwInfo info; - guint8 data[5] = { 0 }; - - g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING, - "*too short*"); - - gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data), - VCSFW_STATUS_OK, &info); - - g_test_assert_expected_messages (); - - g_assert_false (ok); -} - -/* ================================================================ - * T3.3: test_xpfwext_file_parse - * - * Create a synthetic .xpfwext file in a temp dir and verify parsing. - * Format: header + 0x1A + payload + 256-byte signature - * ================================================================ */ -static void -test_xpfwext_file_parse (void) -{ - g_autoptr(GError) error = NULL; - ValidityFwextFile fwext; - gchar *tmpdir; - g_autofree gchar *path = NULL; - - tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); - g_assert_no_error (error); - - path = g_build_filename (tmpdir, "test.xpfwext", NULL); - - /* Build file: "HDR" + 0x1A + 8 bytes payload + 256 bytes sig */ - gsize header_len = 4; /* "HDR" + 0x1A */ - gsize payload_len = 8; - gsize sig_len = 256; - gsize total = header_len + payload_len + sig_len; - - guint8 *content = g_malloc0 (total); - - content[0] = 'H'; - content[1] = 'D'; - content[2] = 'R'; - content[3] = 0x1A; - - /* Payload: 0x01..0x08 */ - for (gsize i = 0; i < payload_len; i++) - content[header_len + i] = (guint8) (i + 1); - - /* Signature: 0xAA repeated */ - memset (content + header_len + payload_len, 0xAA, sig_len); - - g_file_set_contents (path, (gchar *) content, total, &error); - g_assert_no_error (error); - - gboolean ok = validity_fwext_load_file (path, &fwext, &error); - g_assert_no_error (error); - g_assert_true (ok); - - g_assert_cmpuint (fwext.payload_len, ==, payload_len); - - /* Check payload content */ - for (gsize i = 0; i < payload_len; i++) - g_assert_cmpuint (fwext.payload[i], ==, i + 1); - - /* Check signature */ - for (gsize i = 0; i < sig_len; i++) - g_assert_cmpuint (fwext.signature[i], ==, 0xAA); - - validity_fwext_file_clear (&fwext); - g_assert_null (fwext.payload); - g_assert_cmpuint (fwext.payload_len, ==, 0); - - /* Cleanup */ - g_unlink (path); - g_rmdir (tmpdir); - g_free (content); - g_free (tmpdir); -} - -/* ================================================================ - * T3.3b: test_xpfwext_file_no_delimiter - * - * Verify that a file without 0x1A delimiter fails to parse. - * ================================================================ */ -static void -test_xpfwext_file_no_delimiter (void) -{ - g_autoptr(GError) error = NULL; - ValidityFwextFile fwext; - gchar *tmpdir; - g_autofree gchar *path = NULL; - - tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); - g_assert_no_error (error); - - path = g_build_filename (tmpdir, "bad.xpfwext", NULL); - - /* All 0xFF — no 0x1A delimiter */ - guint8 content[300]; - - memset (content, 0xFF, sizeof (content)); - - g_file_set_contents (path, (gchar *) content, sizeof (content), &error); - g_assert_no_error (error); - - gboolean ok = validity_fwext_load_file (path, &fwext, &error); - g_assert_false (ok); - g_assert_nonnull (error); - - g_unlink (path); - g_rmdir (tmpdir); - g_free (tmpdir); -} - -/* ================================================================ - * T3.3c: test_xpfwext_file_too_short - * - * Verify that a file with valid header but data shorter than - * signature size fails. - * ================================================================ */ -static void -test_xpfwext_file_too_short (void) -{ - g_autoptr(GError) error = NULL; - ValidityFwextFile fwext; - gchar *tmpdir; - g_autofree gchar *path = NULL; - - tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); - g_assert_no_error (error); - - path = g_build_filename (tmpdir, "short.xpfwext", NULL); - - /* "X" + 0x1A + 10 bytes (< 257 needed for sig + 1 byte payload) */ - guint8 content[12]; - - content[0] = 'X'; - content[1] = 0x1A; - memset (content + 2, 0, 10); - - g_file_set_contents (path, (gchar *) content, sizeof (content), &error); - g_assert_no_error (error); - - gboolean ok = validity_fwext_load_file (path, &fwext, &error); - g_assert_false (ok); - g_assert_nonnull (error); - - g_unlink (path); - g_rmdir (tmpdir); - g_free (tmpdir); -} - -/* ================================================================ - * T3.4: test_flash_write_cmd_format - * - * Verify that build_write_flash produces the correct wire format: - * [0x41, partition, 1, 0, 0, offset_LE32, len_LE32, data...] - * ================================================================ */ -static void -test_flash_write_cmd_format (void) -{ - guint8 payload[] = { 0xDE, 0xAD, 0xBE, 0xEF }; - guint8 cmd[13 + sizeof (payload)]; - gsize cmd_len; - - validity_fwext_build_write_flash (2, 0x1000, payload, sizeof (payload), - cmd, &cmd_len); - - g_assert_cmpuint (cmd_len, ==, 13 + sizeof (payload)); - g_assert_cmpuint (cmd[0], ==, 0x41); /* WRITE_FLASH command */ - g_assert_cmpuint (cmd[1], ==, 2); /* partition */ - g_assert_cmpuint (cmd[2], ==, 1); /* flag */ - g_assert_cmpuint (cmd[3], ==, 0); /* reserved low */ - g_assert_cmpuint (cmd[4], ==, 0); /* reserved high */ - - /* offset = 0x1000 LE */ - g_assert_cmpuint (cmd[5], ==, 0x00); - g_assert_cmpuint (cmd[6], ==, 0x10); - g_assert_cmpuint (cmd[7], ==, 0x00); - g_assert_cmpuint (cmd[8], ==, 0x00); - - /* length = 4 LE */ - g_assert_cmpuint (cmd[9], ==, 0x04); - g_assert_cmpuint (cmd[10], ==, 0x00); - g_assert_cmpuint (cmd[11], ==, 0x00); - g_assert_cmpuint (cmd[12], ==, 0x00); - - /* data */ - g_assert_cmpmem (cmd + 13, sizeof (payload), payload, sizeof (payload)); -} - -/* ================================================================ - * T3.5: test_fw_sig_cmd_format - * - * Verify that build_write_fw_sig produces the correct wire format: - * [0x42, partition, 0, len_LE16, signature...] - * ================================================================ */ -static void -test_fw_sig_cmd_format (void) -{ - guint8 sig[256]; - guint8 cmd[5 + 256]; - gsize cmd_len; - - memset (sig, 0xBB, sizeof (sig)); - - validity_fwext_build_write_fw_sig (2, sig, sizeof (sig), cmd, &cmd_len); - - g_assert_cmpuint (cmd_len, ==, 5 + 256); - g_assert_cmpuint (cmd[0], ==, 0x42); /* WRITE_FW_SIG command */ - g_assert_cmpuint (cmd[1], ==, 2); /* partition */ - g_assert_cmpuint (cmd[2], ==, 0); /* reserved */ - - /* sig length = 256 LE */ - g_assert_cmpuint (cmd[3], ==, 0x00); - g_assert_cmpuint (cmd[4], ==, 0x01); - - g_assert_cmpmem (cmd + 5, 256, sig, 256); -} - -/* ================================================================ - * T3.6: test_chunk_iteration - * - * Verify that building write_flash with increasing offsets covers - * the entire payload. Simulate the chunk loop used by the SSM. - * ================================================================ */ -static void -test_chunk_iteration (void) -{ - gsize payload_len = 0x2800; /* 10 KB = 2.5 chunks of 4 KB */ - gsize write_offset = 0; - guint chunk_count = 0; - const gsize CHUNK_SIZE = 0x1000; - - while (write_offset < payload_len) - { - gsize remaining = payload_len - write_offset; - gsize chunk_size = MIN (remaining, CHUNK_SIZE); - - g_assert_cmpuint (chunk_size, >, 0); - g_assert_cmpuint (chunk_size, <=, CHUNK_SIZE); - - write_offset += chunk_size; - chunk_count++; - } - - g_assert_cmpuint (write_offset, ==, payload_len); - g_assert_cmpuint (chunk_count, ==, 3); /* 4096 + 4096 + 2048 */ -} - -/* ================================================================ - * T3.7: test_hw_reg_cmd_format - * - * Verify WRITE_HW_REG32 and READ_HW_REG32 command formats. - * ================================================================ */ -static void -test_hw_reg_write_cmd_format (void) -{ - guint8 cmd[10]; - gsize cmd_len; - - validity_fwext_build_write_hw_reg32 (0x8000205C, 7, cmd, &cmd_len); - - g_assert_cmpuint (cmd_len, ==, 10); - g_assert_cmpuint (cmd[0], ==, 0x08); /* WRITE_HW_REG32 command */ - - /* address = 0x8000205C LE */ - g_assert_cmpuint (cmd[1], ==, 0x5C); - g_assert_cmpuint (cmd[2], ==, 0x20); - g_assert_cmpuint (cmd[3], ==, 0x00); - g_assert_cmpuint (cmd[4], ==, 0x80); - - /* value = 7 LE */ - g_assert_cmpuint (cmd[5], ==, 0x07); - g_assert_cmpuint (cmd[6], ==, 0x00); - g_assert_cmpuint (cmd[7], ==, 0x00); - g_assert_cmpuint (cmd[8], ==, 0x00); - - /* size = 4 */ - g_assert_cmpuint (cmd[9], ==, 4); -} - -static void -test_hw_reg_read_cmd_format (void) -{ - guint8 cmd[6]; - gsize cmd_len; - - validity_fwext_build_read_hw_reg32 (0x80002080, cmd, &cmd_len); - - g_assert_cmpuint (cmd_len, ==, 6); - g_assert_cmpuint (cmd[0], ==, 0x07); /* READ_HW_REG32 command */ - - /* address = 0x80002080 LE */ - g_assert_cmpuint (cmd[1], ==, 0x80); - g_assert_cmpuint (cmd[2], ==, 0x20); - g_assert_cmpuint (cmd[3], ==, 0x00); - g_assert_cmpuint (cmd[4], ==, 0x80); - - /* size = 4 */ - g_assert_cmpuint (cmd[5], ==, 4); -} - -static void -test_hw_reg_read_parse (void) -{ - guint32 value; - guint8 data[] = { 0x02, 0x00, 0x00, 0x00 }; - - gboolean ok = validity_fwext_parse_read_hw_reg32 (data, sizeof (data), - &value); - - g_assert_true (ok); - g_assert_cmpuint (value, ==, 2); - - /* Too short should fail */ - ok = validity_fwext_parse_read_hw_reg32 (data, 3, &value); - g_assert_false (ok); -} - -/* ================================================================ - * T3.8: test_firmware_filename - * - * Verify firmware filename mapping for known PIDs. - * ================================================================ */ -static void -test_firmware_filename (void) -{ - const gchar *name; - - name = validity_fwext_get_firmware_name (0x06cb, 0x009a); - g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); - - name = validity_fwext_get_firmware_name (0x138a, 0x0090); - g_assert_cmpstr (name, ==, "6_07f_Lenovo.xpfwext"); - - name = validity_fwext_get_firmware_name (0x138a, 0x0097); - g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); - - name = validity_fwext_get_firmware_name (0x138a, 0x009d); - g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); - - /* Unknown PID should return NULL */ - name = validity_fwext_get_firmware_name (0x1234, 0x5678); - g_assert_null (name); -} - -/* ================================================================ - * T3.9: test_missing_firmware_file - * - * Verify that find_firmware returns an error when the file is not - * found in any search path. - * ================================================================ */ -static void -test_missing_firmware_file (void) -{ - g_autoptr(GError) error = NULL; - - /* This should fail since firmware files aren't installed in CI */ - g_autofree gchar *path = validity_fwext_find_firmware (0x06cb, 0x009a, - &error); - - /* It either found a file or returned an error — both are valid. - * In CI, it should be an error. On a real system, it might succeed. */ - if (path == NULL) - { - g_assert_nonnull (error); - g_assert_true (g_error_matches (error, FP_DEVICE_ERROR, - FP_DEVICE_ERROR_DATA_NOT_FOUND)); - } - else - { - g_assert_no_error (error); - g_assert_true (g_file_test (path, G_FILE_TEST_IS_REGULAR)); - } -} - -/* ================================================================ - * T3.9b: test_unsupported_pid_firmware - * - * Verify that find_firmware for an unknown PID returns NOT_SUPPORTED. - * ================================================================ */ -static void -test_unsupported_pid_firmware (void) -{ - g_autoptr(GError) error = NULL; - g_autofree gchar *path = validity_fwext_find_firmware (0x1234, 0x5678, - &error); - - g_assert_null (path); - g_assert_nonnull (error); - g_assert_true (g_error_matches (error, FP_DEVICE_ERROR, - FP_DEVICE_ERROR_NOT_SUPPORTED)); -} - -/* ================================================================ - * T3.10: test_db_write_enable_blob - * - * Verify that db_write_enable blob is returned for supported PID - * and NULL for unsupported PID. - * ================================================================ */ -static void -test_db_write_enable_blob (void) -{ - gsize len; - const guint8 *blob; - - blob = validity_fwext_get_db_write_enable (0x06cb, 0x009a, &len); - g_assert_nonnull (blob); - g_assert_cmpuint (len, ==, 3621); - - blob = validity_fwext_get_db_write_enable (0x1234, 0x5678, &len); - g_assert_null (blob); - g_assert_cmpuint (len, ==, 0); -} - -/* ================================================================ - * T3.11: test_reboot_cmd_format - * - * Verify reboot command: 0x05, 0x02, 0x00 - * ================================================================ */ -static void -test_reboot_cmd_format (void) -{ - guint8 cmd[3]; - gsize cmd_len; - - validity_fwext_build_reboot (cmd, &cmd_len); - - g_assert_cmpuint (cmd_len, ==, 3); - g_assert_cmpuint (cmd[0], ==, 0x05); - g_assert_cmpuint (cmd[1], ==, 0x02); - g_assert_cmpuint (cmd[2], ==, 0x00); -} - -/* ================================================================ - * Regression: fwext_file_clear on already-cleared struct - * - * Double-free guard. - * ================================================================ */ -static void -test_file_clear_idempotent (void) -{ - ValidityFwextFile fwext = { 0 }; - - /* Clear an empty struct — should not crash */ - validity_fwext_file_clear (&fwext); - validity_fwext_file_clear (&fwext); - - g_assert_null (fwext.payload); - g_assert_cmpuint (fwext.payload_len, ==, 0); -} - -int -main (int argc, - char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - /* Firmware info parsing */ - g_test_add_func ("/validity/fwext/fw-info/parse-present", - test_fw_info_parse_present); - g_test_add_func ("/validity/fwext/fw-info/parse-absent", - test_fw_info_parse_absent); - g_test_add_func ("/validity/fwext/fw-info/parse-unknown-status", - test_fw_info_parse_unknown_status); - g_test_add_func ("/validity/fwext/fw-info/parse-truncated", - test_fw_info_parse_truncated); - - /* File parsing */ - g_test_add_func ("/validity/fwext/file/parse", - test_xpfwext_file_parse); - g_test_add_func ("/validity/fwext/file/no-delimiter", - test_xpfwext_file_no_delimiter); - g_test_add_func ("/validity/fwext/file/too-short", - test_xpfwext_file_too_short); - g_test_add_func ("/validity/fwext/file/clear-idempotent", - test_file_clear_idempotent); - - /* Command format */ - g_test_add_func ("/validity/fwext/cmd/write-flash", - test_flash_write_cmd_format); - g_test_add_func ("/validity/fwext/cmd/write-fw-sig", - test_fw_sig_cmd_format); - g_test_add_func ("/validity/fwext/cmd/write-hw-reg", - test_hw_reg_write_cmd_format); - g_test_add_func ("/validity/fwext/cmd/read-hw-reg", - test_hw_reg_read_cmd_format); - g_test_add_func ("/validity/fwext/cmd/read-hw-reg-parse", - test_hw_reg_read_parse); - g_test_add_func ("/validity/fwext/cmd/reboot", - test_reboot_cmd_format); - - /* Chunk iteration */ - g_test_add_func ("/validity/fwext/chunk-iteration", - test_chunk_iteration); - - /* Firmware filename mapping */ - g_test_add_func ("/validity/fwext/firmware-name", - test_firmware_filename); - g_test_add_func ("/validity/fwext/find-firmware/missing", - test_missing_firmware_file); - g_test_add_func ("/validity/fwext/find-firmware/unsupported-pid", - test_unsupported_pid_firmware); - - /* Blob lookup */ - g_test_add_func ("/validity/fwext/db-write-enable", - test_db_write_enable_blob); - - return g_test_run (); -} diff --git a/tests/test-validity-hal.c b/tests/test-validity-hal.c deleted file mode 100644 index b05078e6..00000000 --- a/tests/test-validity-hal.c +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Unit tests for validity HAL (device descriptor lookup and flash layout) - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include "drivers/validity/validity.h" -#include "drivers/validity/validity_hal.h" - -/* ================================================================ - * T7.1: HAL lookup by device type — all valid types return non-NULL - * ================================================================ */ -static void -test_hal_lookup_all_types (void) -{ - const guint types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, - VALIDITY_DEV_9A, VALIDITY_DEV_9D }; - - for (guint i = 0; i < G_N_ELEMENTS (types); i++) - { - const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); - g_assert_nonnull (desc); - g_assert_cmpuint (desc->vid, >, 0); - g_assert_cmpuint (desc->pid, >, 0); - } -} - -/* ================================================================ - * T7.2: HAL lookup by PID — all supported VID/PID combos - * ================================================================ */ -static void -test_hal_lookup_by_pid (void) -{ - /* All 4 supported devices */ - struct { guint16 vid; - guint16 pid; - } devices[] = { - { 0x138a, 0x0090 }, - { 0x138a, 0x0097 }, - { 0x06cb, 0x009a }, - { 0x138a, 0x009d }, - }; - - for (guint i = 0; i < G_N_ELEMENTS (devices); i++) - { - const ValidityDeviceDesc *desc = - validity_hal_device_lookup_by_pid (devices[i].vid, devices[i].pid); - g_assert_nonnull (desc); - g_assert_cmpuint (desc->vid, ==, devices[i].vid); - g_assert_cmpuint (desc->pid, ==, devices[i].pid); - } -} - -/* ================================================================ - * T7.3: HAL lookup — invalid type returns NULL - * ================================================================ */ -static void -test_hal_lookup_invalid (void) -{ - const ValidityDeviceDesc *desc = validity_hal_device_lookup (99); - - g_assert_null (desc); -} - -/* ================================================================ - * T7.4: HAL lookup by PID — unknown PID returns NULL - * ================================================================ */ -static void -test_hal_lookup_by_pid_invalid (void) -{ - const ValidityDeviceDesc *desc = - validity_hal_device_lookup_by_pid (0x1234, 0x5678); - - g_assert_null (desc); -} - -/* ================================================================ - * T7.5: All devices have non-empty blobs - * ================================================================ */ -static void -test_hal_blobs_present (void) -{ - const guint types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, - VALIDITY_DEV_9A, VALIDITY_DEV_9D }; - - for (guint i = 0; i < G_N_ELEMENTS (types); i++) - { - const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); - g_assert_nonnull (desc); - - /* init_hardcoded must be present for all */ - g_assert_nonnull (desc->init_hardcoded); - g_assert_cmpuint (desc->init_hardcoded_len, >, 0); - - /* reset_blob must be present for all */ - g_assert_nonnull (desc->reset_blob); - g_assert_cmpuint (desc->reset_blob_len, >, 0); - - /* db_write_enable must be present for all */ - g_assert_nonnull (desc->db_write_enable); - g_assert_cmpuint (desc->db_write_enable_len, >, 0); - } -} - -/* ================================================================ - * T7.6: PID 0090 has smaller db partition and no clean_slate blob - * ================================================================ */ -static void -test_hal_pid_0090_specifics (void) -{ - const ValidityDeviceDesc *desc = validity_hal_device_lookup (VALIDITY_DEV_90); - - g_assert_nonnull (desc); - - /* 0090 has no init_hardcoded_clean_slate */ - g_assert_null (desc->init_clean_slate); - g_assert_cmpuint (desc->init_clean_slate_len, ==, 0); - - /* Flash layout should exist */ - g_assert_nonnull (desc->flash_layout); - g_assert_cmpuint (desc->flash_layout->num_partitions, ==, - VALIDITY_FLASH_NUM_PARTITIONS); -} - -/* ================================================================ - * T7.7: Non-0090 devices have init_hardcoded_clean_slate - * ================================================================ */ -static void -test_hal_clean_slate_present (void) -{ - const guint types[] = { VALIDITY_DEV_97, VALIDITY_DEV_9A, VALIDITY_DEV_9D }; - - for (guint i = 0; i < G_N_ELEMENTS (types); i++) - { - const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); - g_assert_nonnull (desc); - g_assert_nonnull (desc->init_clean_slate); - g_assert_cmpuint (desc->init_clean_slate_len, >, 0); - } -} - -/* ================================================================ - * T7.8: Flash layout has valid partition table - * ================================================================ */ -static void -test_hal_flash_layout (void) -{ - const guint types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, - VALIDITY_DEV_9A, VALIDITY_DEV_9D }; - - for (guint i = 0; i < G_N_ELEMENTS (types); i++) - { - const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); - g_assert_nonnull (desc); - g_assert_nonnull (desc->flash_layout); - - const ValidityFlashLayout *layout = desc->flash_layout; - g_assert_cmpuint (layout->num_partitions, ==, - VALIDITY_FLASH_NUM_PARTITIONS); - - /* Signature must be 256 bytes */ - g_assert_nonnull (layout->partition_sig); - g_assert_cmpuint (layout->partition_sig_len, ==, - VALIDITY_PARTITION_SIG_SIZE); - - /* Verify partitions are ordered and non-overlapping */ - for (guint p = 0; p < layout->num_partitions; p++) - { - const ValidityPartition *part = &layout->partitions[p]; - g_assert_cmpuint (part->size, >, 0); - - if (p > 0) - { - const ValidityPartition *prev = &layout->partitions[p - 1]; - g_assert_cmpuint (part->offset, >=, - prev->offset + prev->size); - } - } - } -} - -/* ================================================================ - * T7.9: Blob sizes match expected values from python-validity - * ================================================================ */ -static void -test_hal_blob_sizes (void) -{ - const ValidityDeviceDesc *desc_9a = - validity_hal_device_lookup (VALIDITY_DEV_9A); - - g_assert_nonnull (desc_9a); - - /* 009a blobs: init=581, clean_slate=741, reset=12037, dbe=3621 */ - g_assert_cmpuint (desc_9a->init_hardcoded_len, ==, 581); - g_assert_cmpuint (desc_9a->init_clean_slate_len, ==, 741); - g_assert_cmpuint (desc_9a->reset_blob_len, ==, 12037); - g_assert_cmpuint (desc_9a->db_write_enable_len, ==, 3621); - - const ValidityDeviceDesc *desc_90 = - validity_hal_device_lookup (VALIDITY_DEV_90); - g_assert_nonnull (desc_90); - - /* 0090 blobs: init=485, no clean_slate, reset=11493, dbe=1765 */ - g_assert_cmpuint (desc_90->init_hardcoded_len, ==, 485); - g_assert_cmpuint (desc_90->reset_blob_len, ==, 11493); - g_assert_cmpuint (desc_90->db_write_enable_len, ==, 1765); -} - -/* ================================================================ - * T7.10: Lookup consistency — by-type and by-PID return same pointer - * ================================================================ */ -static void -test_hal_lookup_consistency (void) -{ - const ValidityDeviceDesc *by_type = - validity_hal_device_lookup (VALIDITY_DEV_9A); - const ValidityDeviceDesc *by_pid = - validity_hal_device_lookup_by_pid (0x06cb, 0x009a); - - g_assert_true (by_type == by_pid); -} - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/validity/hal/lookup-all-types", - test_hal_lookup_all_types); - g_test_add_func ("/validity/hal/lookup-by-pid", - test_hal_lookup_by_pid); - g_test_add_func ("/validity/hal/lookup-invalid", - test_hal_lookup_invalid); - g_test_add_func ("/validity/hal/lookup-by-pid-invalid", - test_hal_lookup_by_pid_invalid); - g_test_add_func ("/validity/hal/blobs-present", - test_hal_blobs_present); - g_test_add_func ("/validity/hal/pid-0090-specifics", - test_hal_pid_0090_specifics); - g_test_add_func ("/validity/hal/clean-slate-present", - test_hal_clean_slate_present); - g_test_add_func ("/validity/hal/flash-layout", - test_hal_flash_layout); - g_test_add_func ("/validity/hal/blob-sizes", - test_hal_blob_sizes); - g_test_add_func ("/validity/hal/lookup-consistency", - test_hal_lookup_consistency); - - return g_test_run (); -} diff --git a/tests/test-validity-pair.c b/tests/test-validity-pair.c deleted file mode 100644 index 6ceba067..00000000 --- a/tests/test-validity-pair.c +++ /dev/null @@ -1,507 +0,0 @@ -/* - * Unit tests for validity pairing helper functions - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "fpi-byte-utils.h" -#include "drivers/validity/validity.h" -#include "drivers/validity/validity_pair.h" -#include "drivers/validity/validity_hal.h" - -/* ================================================================ - * T7.11: parse_flash_info — valid response - * ================================================================ */ -static void -test_parse_flash_info_valid (void) -{ - /* CMD 0x3e response format (after 2-byte status, already stripped): - * [jid0:2LE][jid1:2LE][blocks:2LE][unknown0:2LE][blocksize:2LE] - * [unknown1:2LE][pcnt:2LE] = 14 bytes minimum */ - guint8 data[14]; - - memset (data, 0, sizeof (data)); - - /* jid0=0x01, jid1=0x02, blocks=0x1000, unknown0=0, blocksize=0x100, - * unknown1=0, pcnt=5 (5 partitions = device already paired) */ - FP_WRITE_UINT16_LE (data + 0, 0x0001); /* jid0 */ - FP_WRITE_UINT16_LE (data + 2, 0x0002); /* jid1 */ - FP_WRITE_UINT16_LE (data + 4, 0x1000); /* blocks */ - FP_WRITE_UINT16_LE (data + 6, 0x0000); /* unknown0 */ - FP_WRITE_UINT16_LE (data + 8, 0x0100); /* blocksize */ - FP_WRITE_UINT16_LE (data + 10, 0x0000); /* unknown1 */ - FP_WRITE_UINT16_LE (data + 12, 5); /* pcnt = 5 */ - - ValidityFlashIcParams ic; - guint16 num_partitions; - - gboolean ok = validity_pair_parse_flash_info (data, sizeof (data), - &ic, &num_partitions); - g_assert_true (ok); - g_assert_cmpuint (num_partitions, ==, 5); - g_assert_cmpuint (ic.size, ==, 0x1000 * 0x0100); - g_assert_cmpuint (ic.sector_size, ==, 0x1000); - g_assert_cmpuint (ic.sector_erase_cmd, ==, 0x20); -} - -/* ================================================================ - * T7.12: parse_flash_info — zero partitions means needs pairing - * ================================================================ */ -static void -test_parse_flash_info_needs_pairing (void) -{ - guint8 data[14]; - - memset (data, 0, sizeof (data)); - - FP_WRITE_UINT16_LE (data + 0, 0x0001); - FP_WRITE_UINT16_LE (data + 2, 0x0002); - FP_WRITE_UINT16_LE (data + 4, 0x0800); - FP_WRITE_UINT16_LE (data + 8, 0x0200); - FP_WRITE_UINT16_LE (data + 12, 0); /* 0 partitions */ - - ValidityFlashIcParams ic; - guint16 num_partitions; - - gboolean ok = validity_pair_parse_flash_info (data, sizeof (data), - &ic, &num_partitions); - g_assert_true (ok); - g_assert_cmpuint (num_partitions, ==, 0); -} - -/* ================================================================ - * T7.13: parse_flash_info — too short data fails - * ================================================================ */ -static void -test_parse_flash_info_too_short (void) -{ - guint8 data[10]; /* less than 14 bytes */ - - memset (data, 0, sizeof (data)); - - ValidityFlashIcParams ic; - guint16 num_partitions; - - gboolean ok = validity_pair_parse_flash_info (data, sizeof (data), - &ic, &num_partitions); - g_assert_false (ok); -} - -/* ================================================================ - * T7.14: serialize_partition — known output format - * ================================================================ */ -static void -test_serialize_partition (void) -{ - ValidityPartition part = { - .id = 1, - .type = 3, - .access_lvl = 0x0002, - .offset = 0x1000, - .size = 0x8000, - }; - - guint8 out[VALIDITY_PARTITION_ENTRY_SIZE]; - - validity_pair_serialize_partition (&part, out); - - /* Check first 12 bytes: id(1) type(1) access_lvl(2LE) offset(4LE) size(4LE) */ - g_assert_cmpuint (out[0], ==, 1); - g_assert_cmpuint (out[1], ==, 3); - g_assert_cmpuint (FP_READ_UINT16_LE (out + 2), ==, 0x0002); - g_assert_cmpuint (FP_READ_UINT32_LE (out + 4), ==, 0x1000); - g_assert_cmpuint (FP_READ_UINT32_LE (out + 8), ==, 0x8000); - - /* Bytes 12-15 should be zero */ - for (int i = 12; i < 16; i++) - g_assert_cmpuint (out[i], ==, 0); - - /* Bytes 16-47 = SHA-256 of the 12-byte entry */ - g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA256); - g_checksum_update (checksum, out, 12); - guint8 expected_hash[32]; - gsize hash_len = 32; - g_checksum_get_digest (checksum, expected_hash, &hash_len); - - g_assert_cmpmem (out + 16, 32, expected_hash, 32); - - /* Total size must be VALIDITY_PARTITION_ENTRY_SIZE */ - g_assert_cmpuint (sizeof (out), ==, VALIDITY_PARTITION_ENTRY_SIZE); -} - -/* ================================================================ - * T7.15: make_cert — produces 444-byte certificate - * ================================================================ */ -static void -test_make_cert_size (void) -{ - guint8 pub_x[32], pub_y[32]; - - RAND_bytes (pub_x, 32); - RAND_bytes (pub_y, 32); - - gsize cert_len; - g_autofree guint8 *cert = validity_pair_make_cert (pub_x, pub_y, &cert_len); - - g_assert_nonnull (cert); - g_assert_cmpuint (cert_len, ==, VALIDITY_CLIENT_CERT_SIZE); - - /* First 4 bytes should be 0x17 in LE */ - g_assert_cmpuint (FP_READ_UINT32_LE (cert), ==, 0x17); - /* Bytes 4-7 should be 0x20 in LE */ - g_assert_cmpuint (FP_READ_UINT32_LE (cert + 4), ==, 0x20); -} - -/* ================================================================ - * T7.16: make_cert — deterministic for same input - * ================================================================ */ -static void -test_make_cert_deterministic (void) -{ - /* Using fixed keys so signature is reproducible. - * Note: ECDSA uses random k, so signatures differ — but the - * structure should be consistent. Actually ECDSA is NOT deterministic - * without RFC 6979, so we can only verify structure matches. */ - guint8 pub_x[32] = { 0x01 }; - guint8 pub_y[32] = { 0x02 }; - - gsize len1, len2; - g_autofree guint8 *cert1 = validity_pair_make_cert (pub_x, pub_y, &len1); - g_autofree guint8 *cert2 = validity_pair_make_cert (pub_x, pub_y, &len2); - - g_assert_nonnull (cert1); - g_assert_nonnull (cert2); - g_assert_cmpuint (len1, ==, len2); - g_assert_cmpuint (len1, ==, VALIDITY_CLIENT_CERT_SIZE); - - /* Header and public key portion should be identical (first 184 bytes = cert body) */ - g_assert_cmpmem (cert1, 184, cert2, 184); -} - -/* ================================================================ - * T7.17: encrypt_key — output blob has correct structure - * ================================================================ */ -static void -test_encrypt_key_structure (void) -{ - guint8 priv[32], pub_x[32], pub_y[32]; - guint8 enc_key[32], val_key[32]; - - RAND_bytes (priv, 32); - RAND_bytes (pub_x, 32); - RAND_bytes (pub_y, 32); - RAND_bytes (enc_key, 32); - RAND_bytes (val_key, 32); - - gsize blob_len; - g_autofree guint8 *blob = validity_pair_encrypt_key (priv, pub_x, pub_y, - enc_key, val_key, - &blob_len); - - g_assert_nonnull (blob); - /* Blob format: 0x02(1) + IV(16) + ciphertext(112) + HMAC(32) = 161 */ - g_assert_cmpuint (blob_len, ==, 161); - g_assert_cmpuint (blob[0], ==, VALIDITY_ENCRYPTED_KEY_PREFIX); -} - -/* ================================================================ - * T7.18: encrypt_key — HMAC verification - * ================================================================ */ -static void -test_encrypt_key_hmac_valid (void) -{ - guint8 priv[32], pub_x[32], pub_y[32]; - guint8 enc_key[32], val_key[32]; - - RAND_bytes (priv, 32); - RAND_bytes (pub_x, 32); - RAND_bytes (pub_y, 32); - RAND_bytes (enc_key, 32); - RAND_bytes (val_key, 32); - - gsize blob_len; - g_autofree guint8 *blob = validity_pair_encrypt_key (priv, pub_x, pub_y, - enc_key, val_key, - &blob_len); - - g_assert_nonnull (blob); - g_assert_cmpuint (blob_len, ==, 161); - - /* Blob: [0x02][iv:16][ct:112][hmac:32] - * HMAC is over iv+ct (bytes 1..128) */ - const guint8 *iv_ct = blob + 1; - gsize iv_ct_len = 16 + 112; - const guint8 *stored_hmac = blob + 1 + iv_ct_len; - - guint8 computed_hmac[32]; - guint hmac_len = 32; - HMAC (EVP_sha256 (), val_key, 32, iv_ct, iv_ct_len, - computed_hmac, &hmac_len); - - g_assert_cmpmem (stored_hmac, 32, computed_hmac, 32); -} - -/* ================================================================ - * T7.19: build_partition_flash_cmd — valid output structure - * ================================================================ */ -static void -test_build_partition_flash_cmd (void) -{ - /* Use a real device descriptor for the flash layout */ - const ValidityDeviceDesc *desc = - validity_hal_device_lookup (VALIDITY_DEV_9A); - - g_assert_nonnull (desc); - - ValidityFlashIcParams flash_ic = { - .size = 0x200000, - .sector_size = 0x1000, - .sector_erase_cmd = 0x20, - }; - - guint8 pub_x[32], pub_y[32]; - RAND_bytes (pub_x, 32); - RAND_bytes (pub_y, 32); - - gsize cmd_len; - g_autofree guint8 *cmd = - validity_pair_build_partition_flash_cmd (&flash_ic, desc->flash_layout, - pub_x, pub_y, &cmd_len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (cmd_len, >, 5); - - /* Command prefix: 0x4f followed by 4 zero bytes */ - g_assert_cmpuint (cmd[0], ==, 0x4f); - g_assert_cmpuint (cmd[1], ==, 0); - g_assert_cmpuint (cmd[2], ==, 0); - g_assert_cmpuint (cmd[3], ==, 0); - g_assert_cmpuint (cmd[4], ==, 0); -} - -/* ================================================================ - * T7.20: build_tls_flash — produces exactly 4096 bytes - * ================================================================ */ -static void -test_build_tls_flash_size (void) -{ - ValidityPairState state; - - validity_pair_state_init (&state); - - /* Set up minimal test data */ - guint8 priv_blob[100]; - guint8 server_cert[200]; - guint8 ecdh_blob[400]; - RAND_bytes (priv_blob, sizeof (priv_blob)); - RAND_bytes (server_cert, sizeof (server_cert)); - RAND_bytes (ecdh_blob, sizeof (ecdh_blob)); - - state.priv_blob = priv_blob; - state.priv_blob_len = sizeof (priv_blob); - state.server_cert = server_cert; - state.server_cert_len = sizeof (server_cert); - state.ecdh_blob = ecdh_blob; - state.ecdh_blob_len = sizeof (ecdh_blob); - - gsize flash_len; - g_autofree guint8 *flash = validity_pair_build_tls_flash (&state, &flash_len); - - g_assert_nonnull (flash); - g_assert_cmpuint (flash_len, ==, 0x1000); - - /* Verify padding bytes at end are 0xff */ - gboolean has_ff_padding = FALSE; - for (gsize i = flash_len - 1; i > 0; i--) - { - if (flash[i] == 0xff) - { - has_ff_padding = TRUE; - break; - } - } - g_assert_true (has_ff_padding); - - /* Don't free embedded pointers since they're stack-allocated */ - state.priv_blob = NULL; - state.server_cert = NULL; - state.ecdh_blob = NULL; - validity_pair_state_free (&state); -} - -/* ================================================================ - * T7.21: build_tls_flash — block structure - * ================================================================ */ -static void -test_build_tls_flash_blocks (void) -{ - ValidityPairState state; - - validity_pair_state_init (&state); - - guint8 priv_blob[50]; - guint8 server_cert[100]; - guint8 ecdh_blob[400]; - memset (priv_blob, 0xAA, sizeof (priv_blob)); - memset (server_cert, 0xBB, sizeof (server_cert)); - memset (ecdh_blob, 0xCC, sizeof (ecdh_blob)); - - state.priv_blob = priv_blob; - state.priv_blob_len = sizeof (priv_blob); - state.server_cert = server_cert; - state.server_cert_len = sizeof (server_cert); - state.ecdh_blob = ecdh_blob; - state.ecdh_blob_len = sizeof (ecdh_blob); - - gsize flash_len; - g_autofree guint8 *flash = validity_pair_build_tls_flash (&state, &flash_len); - g_assert_nonnull (flash); - - /* Block 0 should be first: id=0, size=1 */ - g_assert_cmpuint (FP_READ_UINT16_LE (flash), ==, 0); /* block id */ - g_assert_cmpuint (FP_READ_UINT16_LE (flash + 2), ==, 1); /* size = 1 */ - /* Skip 32-byte hash at flash+4 and 1-byte body at flash+36 */ - - /* Next block should be block 4 (priv_blob) at offset 37 */ - gsize offset = 4 + 32 + 1; /* header(4) + hash(32) + body(1) */ - g_assert_cmpuint (FP_READ_UINT16_LE (flash + offset), ==, 4); /* block id */ - g_assert_cmpuint (FP_READ_UINT16_LE (flash + offset + 2), ==, - sizeof (priv_blob)); - - state.priv_blob = NULL; - state.server_cert = NULL; - state.ecdh_blob = NULL; - validity_pair_state_free (&state); -} - -/* ================================================================ - * T7.22: pair state init and free - * ================================================================ */ -static void -test_pair_state_lifecycle (void) -{ - ValidityPairState state; - - validity_pair_state_init (&state); - - g_assert_null (state.client_key); - g_assert_null (state.server_cert); - g_assert_null (state.ecdh_blob); - g_assert_null (state.priv_blob); - g_assert_cmpuint (state.num_partitions, ==, 0); - g_assert_cmpuint (state.erase_step, ==, 0); - - /* Free should be safe on empty state */ - validity_pair_state_free (&state); -} - -/* ================================================================ - * T7.23: pair state free with allocated resources - * ================================================================ */ -static void -test_pair_state_free_with_resources (void) -{ - ValidityPairState state; - - validity_pair_state_init (&state); - - state.server_cert = g_malloc (100); - state.server_cert_len = 100; - state.ecdh_blob = g_malloc (400); - state.ecdh_blob_len = 400; - state.priv_blob = g_malloc (161); - state.priv_blob_len = 161; - - /* Generate a key to test EVP_PKEY_free path */ - EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id (EVP_PKEY_EC, NULL); - EVP_PKEY_keygen_init (pctx); - EVP_PKEY_CTX_set_ec_paramgen_curve_nid (pctx, NID_X9_62_prime256v1); - EVP_PKEY_keygen (pctx, &state.client_key); - EVP_PKEY_CTX_free (pctx); - g_assert_nonnull (state.client_key); - - /* Free should release all resources without leak */ - validity_pair_state_free (&state); -} - -/* ================================================================ - * T7.24: encrypt_key — different inputs produce different blobs - * ================================================================ */ -static void -test_encrypt_key_different_inputs (void) -{ - guint8 priv1[32], priv2[32], pub_x[32], pub_y[32]; - guint8 enc_key[32], val_key[32]; - - RAND_bytes (priv1, 32); - RAND_bytes (priv2, 32); - RAND_bytes (pub_x, 32); - RAND_bytes (pub_y, 32); - RAND_bytes (enc_key, 32); - RAND_bytes (val_key, 32); - - gsize len1, len2; - g_autofree guint8 *blob1 = validity_pair_encrypt_key (priv1, pub_x, pub_y, - enc_key, val_key, &len1); - g_autofree guint8 *blob2 = validity_pair_encrypt_key (priv2, pub_x, pub_y, - enc_key, val_key, &len2); - - g_assert_nonnull (blob1); - g_assert_nonnull (blob2); - g_assert_cmpuint (len1, ==, len2); - - /* Different private keys should produce different ciphertexts */ - g_assert_true (memcmp (blob1, blob2, len1) != 0); -} - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/validity/pair/parse-flash-info-valid", - test_parse_flash_info_valid); - g_test_add_func ("/validity/pair/parse-flash-info-needs-pairing", - test_parse_flash_info_needs_pairing); - g_test_add_func ("/validity/pair/parse-flash-info-too-short", - test_parse_flash_info_too_short); - g_test_add_func ("/validity/pair/serialize-partition", - test_serialize_partition); - g_test_add_func ("/validity/pair/make-cert-size", - test_make_cert_size); - g_test_add_func ("/validity/pair/make-cert-deterministic", - test_make_cert_deterministic); - g_test_add_func ("/validity/pair/encrypt-key-structure", - test_encrypt_key_structure); - g_test_add_func ("/validity/pair/encrypt-key-hmac-valid", - test_encrypt_key_hmac_valid); - g_test_add_func ("/validity/pair/build-partition-flash-cmd", - test_build_partition_flash_cmd); - g_test_add_func ("/validity/pair/build-tls-flash-size", - test_build_tls_flash_size); - g_test_add_func ("/validity/pair/build-tls-flash-blocks", - test_build_tls_flash_blocks); - g_test_add_func ("/validity/pair/state-lifecycle", - test_pair_state_lifecycle); - g_test_add_func ("/validity/pair/state-free-with-resources", - test_pair_state_free_with_resources); - g_test_add_func ("/validity/pair/encrypt-key-different-inputs", - test_encrypt_key_different_inputs); - - return g_test_run (); -} diff --git a/tests/test-validity-sensor.c b/tests/test-validity-sensor.c deleted file mode 100644 index e33ffe7d..00000000 --- a/tests/test-validity-sensor.c +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Unit tests for validity sensor identification and HAL tables - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include "fpi-byte-utils.h" - -#include "drivers/validity/validity_sensor.h" - -/* ================================================================ - * T4.1: test_identify_sensor_parse - * - * Verify that a valid cmd 0x75 response is parsed correctly into - * a ValiditySensorIdent (hw_major + hw_version). - * - * Wire format (after 2-byte status stripped): - * [zeroes:4 LE] [version:2 LE] [major:2 LE] - * ================================================================ */ -static void -test_identify_sensor_parse (void) -{ - ValiditySensorIdent ident; - - /* Build synthetic response: zeroes=0, version=0x13, major=0x004a */ - guint8 data[8]; - - FP_WRITE_UINT32_LE (&data[0], 0); /* zeroes */ - FP_WRITE_UINT16_LE (&data[4], 0x0013); /* version */ - FP_WRITE_UINT16_LE (&data[6], 0x004a); /* major */ - - gboolean ok = validity_sensor_parse_identify (data, sizeof (data), &ident); - - g_assert_true (ok); - g_assert_cmpuint (ident.hw_major, ==, 0x004a); - g_assert_cmpuint (ident.hw_version, ==, 0x0013); -} - -/* ================================================================ - * T4.2: test_identify_sensor_parse_truncated - * - * Verify that a response shorter than 8 bytes returns FALSE. - * ================================================================ */ -static void -test_identify_sensor_parse_truncated (void) -{ - ValiditySensorIdent ident; - - guint8 data[7] = { 0 }; - - g_assert_false (validity_sensor_parse_identify (data, sizeof (data), &ident)); - /* Also test with 0 length */ - g_assert_false (validity_sensor_parse_identify (data, 0, &ident)); -} - -/* ================================================================ - * T4.3: test_device_info_lookup_exact - * - * Verify that lookup with major=0x004a, version=0x13 returns the - * correct DeviceInfo for the ThinkPad T480s sensor. - * ================================================================ */ -static void -test_device_info_lookup_exact (void) -{ - const ValidityDeviceInfo *info; - - info = validity_device_info_lookup (0x004a, 0x13); - - g_assert_nonnull (info); - g_assert_cmpuint (info->major, ==, 0x004a); - g_assert_cmpuint (info->type, ==, 0x00b5); - g_assert_cmpuint (info->version, ==, 0x13); - g_assert_cmpstr (info->name, ==, "SYN 57K0 FM3297-02"); -} - -/* ================================================================ - * T4.4: test_device_info_lookup_another - * - * Verify that lookup with major=0x0071, version=0x01 returns - * the VSI 55E entry (type 0xdb). - * ================================================================ */ -static void -test_device_info_lookup_another (void) -{ - const ValidityDeviceInfo *info; - - info = validity_device_info_lookup (0x0071, 0x01); - - g_assert_nonnull (info); - g_assert_cmpuint (info->type, ==, 0x00db); - g_assert_cmpstr (info->name, ==, "VSI 55E FM72-001"); -} - -/* ================================================================ - * T4.5: test_device_info_lookup_unknown - * - * Verify that a completely unknown major returns NULL. - * ================================================================ */ -static void -test_device_info_lookup_unknown (void) -{ - const ValidityDeviceInfo *info; - - info = validity_device_info_lookup (0xffff, 0x01); - - g_assert_null (info); -} - -/* ================================================================ - * T4.6: test_device_info_lookup_fuzzy - * - * Verify that when version_mask == 0x00, the entry matches any - * version (fuzzy match). - * ================================================================ */ -static void -test_device_info_lookup_fuzzy (void) -{ - const ValidityDeviceInfo *info; - - /* major=0x0000 entries have version_mask=0x00 → always fuzzy match. - * But major=0x0000 needs to match the lookup major. */ - info = validity_device_info_lookup (0x0000, 0x42); - - /* Should match one of the wildcard entries */ - g_assert_nonnull (info); - g_assert_cmpuint (info->major, ==, 0x0000); -} - -/* ================================================================ - * T4.7: test_sensor_type_info_lookup - * - * Verify lookup of sensor type 0x00b5 returns correct geometry. - * ================================================================ */ -static void -test_sensor_type_info_lookup (void) -{ - const ValiditySensorTypeInfo *info; - - info = validity_sensor_type_info_lookup (0x00b5); - - g_assert_nonnull (info); - g_assert_cmpuint (info->sensor_type, ==, 0x00b5); - g_assert_cmpuint (info->bytes_per_line, ==, 0x78); - g_assert_cmpuint (info->repeat_multiplier, ==, 2); - g_assert_cmpuint (info->lines_per_calibration_data, ==, 112); - g_assert_cmpuint (info->line_width, ==, 112); - g_assert_nonnull (info->calibration_blob); - g_assert_cmpuint (info->calibration_blob_len, ==, 112); -} - -/* ================================================================ - * T4.8: test_sensor_type_info_lookup_db - * - * Verify lookup of sensor type 0x00db (55E) returns correct geometry. - * ================================================================ */ -static void -test_sensor_type_info_lookup_db (void) -{ - const ValiditySensorTypeInfo *info; - - info = validity_sensor_type_info_lookup (0x00db); - - g_assert_nonnull (info); - g_assert_cmpuint (info->bytes_per_line, ==, 0x98); - g_assert_cmpuint (info->repeat_multiplier, ==, 1); - g_assert_cmpuint (info->lines_per_calibration_data, ==, 144); - g_assert_cmpuint (info->line_width, ==, 144); -} - -/* ================================================================ - * T4.9: test_sensor_type_info_lookup_unknown - * - * Verify that an unknown sensor type returns NULL. - * ================================================================ */ -static void -test_sensor_type_info_lookup_unknown (void) -{ - g_assert_null (validity_sensor_type_info_lookup (0xbeef)); -} - -/* ================================================================ - * T4.10: test_factory_bits_cmd_format - * - * Verify that the factory bits command is built correctly. - * Expected: [0x6f] [0x00 0x0e] [0x00 0x00] [0x00 0x00 0x00 0x00] - * ================================================================ */ -static void -test_factory_bits_cmd_format (void) -{ - guint8 buf[16]; - gsize len; - - len = validity_sensor_build_factory_bits_cmd (0x0e00, buf, sizeof (buf)); - - g_assert_cmpuint (len, ==, 9); - g_assert_cmpuint (buf[0], ==, 0x6f); - /* tag = 0x0e00 LE */ - g_assert_cmpuint (buf[1], ==, 0x00); - g_assert_cmpuint (buf[2], ==, 0x0e); - /* pad 2 bytes */ - g_assert_cmpuint (buf[3], ==, 0x00); - g_assert_cmpuint (buf[4], ==, 0x00); - /* pad 4 bytes */ - g_assert_cmpuint (buf[5], ==, 0x00); - g_assert_cmpuint (buf[6], ==, 0x00); - g_assert_cmpuint (buf[7], ==, 0x00); - g_assert_cmpuint (buf[8], ==, 0x00); -} - -/* ================================================================ - * T4.11: test_factory_bits_cmd_buffer_too_small - * - * Verify that a too-small buffer returns 0. - * ================================================================ */ -static void -test_factory_bits_cmd_buffer_too_small (void) -{ - guint8 buf[4]; - gsize len; - - len = validity_sensor_build_factory_bits_cmd (0x0e00, buf, sizeof (buf)); - - g_assert_cmpuint (len, ==, 0); -} - -/* ================================================================ - * T4.12: test_identify_then_lookup - * - * End-to-end: parse identify_sensor response → DeviceInfo lookup → - * SensorTypeInfo lookup. Simulates the T480s sensor (06cb:009a). - * ================================================================ */ -static void -test_identify_then_lookup (void) -{ - ValiditySensorIdent ident; - const ValidityDeviceInfo *dev_info; - const ValiditySensorTypeInfo *type_info; - - /* Simulate cmd 0x75 response for T480s: major=0x004a, version=0x13 */ - guint8 data[8]; - - FP_WRITE_UINT32_LE (&data[0], 0); - FP_WRITE_UINT16_LE (&data[4], 0x0013); - FP_WRITE_UINT16_LE (&data[6], 0x004a); - - g_assert_true (validity_sensor_parse_identify (data, sizeof (data), &ident)); - g_assert_cmpuint (ident.hw_major, ==, 0x004a); - g_assert_cmpuint (ident.hw_version, ==, 0x0013); - - dev_info = validity_device_info_lookup (ident.hw_major, ident.hw_version); - g_assert_nonnull (dev_info); - g_assert_cmpuint (dev_info->type, ==, 0x00b5); - - type_info = validity_sensor_type_info_lookup (dev_info->type); - g_assert_nonnull (type_info); - g_assert_cmpuint (type_info->bytes_per_line, ==, 0x78); - g_assert_cmpuint (type_info->line_width, ==, 112); -} - -/* ================================================================ - * T4.13: test_sensor_state_lifecycle - * - * Verify that init zeros the state and clear frees allocated data. - * ================================================================ */ -static void -test_sensor_state_lifecycle (void) -{ - ValiditySensorState state; - - validity_sensor_state_init (&state); - g_assert_null (state.device_info); - g_assert_null (state.type_info); - g_assert_null (state.factory_bits); - g_assert_cmpuint (state.factory_bits_len, ==, 0); - - /* Simulate storing factory bits */ - state.factory_bits = g_memdup2 ("\x01\x02\x03", 3); - state.factory_bits_len = 3; - - validity_sensor_state_clear (&state); - g_assert_null (state.factory_bits); - g_assert_cmpuint (state.factory_bits_len, ==, 0); -} - -/* ================================================================ - * T4.14: test_calibration_blob_present - * - * Verify that the calibration blob for type 0x00b5 has expected - * first and last bytes (from python-validity generated_tables). - * ================================================================ */ -static void -test_calibration_blob_present (void) -{ - const ValiditySensorTypeInfo *info; - - info = validity_sensor_type_info_lookup (0x00b5); - g_assert_nonnull (info); - g_assert_nonnull (info->calibration_blob); - g_assert_cmpuint (info->calibration_blob_len, ==, 112); - - /* First byte: 0x9b, last byte: 0x06 */ - g_assert_cmpuint (info->calibration_blob[0], ==, 0x9b); - g_assert_cmpuint (info->calibration_blob[111], ==, 0x06); -} - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/validity/sensor/identify/parse", - test_identify_sensor_parse); - g_test_add_func ("/validity/sensor/identify/truncated", - test_identify_sensor_parse_truncated); - g_test_add_func ("/validity/sensor/devinfo/lookup_exact", - test_device_info_lookup_exact); - g_test_add_func ("/validity/sensor/devinfo/lookup_another", - test_device_info_lookup_another); - g_test_add_func ("/validity/sensor/devinfo/lookup_unknown", - test_device_info_lookup_unknown); - g_test_add_func ("/validity/sensor/devinfo/lookup_fuzzy", - test_device_info_lookup_fuzzy); - g_test_add_func ("/validity/sensor/typeinfo/lookup", - test_sensor_type_info_lookup); - g_test_add_func ("/validity/sensor/typeinfo/lookup_db", - test_sensor_type_info_lookup_db); - g_test_add_func ("/validity/sensor/typeinfo/lookup_unknown", - test_sensor_type_info_lookup_unknown); - g_test_add_func ("/validity/sensor/factory_bits/cmd_format", - test_factory_bits_cmd_format); - g_test_add_func ("/validity/sensor/factory_bits/buffer_too_small", - test_factory_bits_cmd_buffer_too_small); - g_test_add_func ("/validity/sensor/identify_then_lookup", - test_identify_then_lookup); - g_test_add_func ("/validity/sensor/state_lifecycle", - test_sensor_state_lifecycle); - g_test_add_func ("/validity/sensor/calibration_blob_present", - test_calibration_blob_present); - - return g_test_run (); -} diff --git a/tests/test-validity-tls.c b/tests/test-validity-tls.c deleted file mode 100644 index 10eadbd8..00000000 --- a/tests/test-validity-tls.c +++ /dev/null @@ -1,789 +0,0 @@ -/* - * Unit tests for validity TLS session management functions - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include -#include -#include - -#include "fpi-device.h" -#include "fpi-ssm.h" -#include "fpi-byte-reader.h" - -/* We include the TLS header and use function declarations directly. - * The test links against the driver static lib. */ -#include "drivers/validity/validity_tls.h" -#include "drivers/validity/vcsfw_protocol.h" - -/* ================================================================ - * Test: PRF produces deterministic output - * ================================================================ */ -static void -test_prf_deterministic (void) -{ - guint8 secret[] = { 0x01, 0x02, 0x03, 0x04 }; - guint8 seed[] = { 0x05, 0x06, 0x07, 0x08 }; - guint8 output1[48]; - guint8 output2[48]; - - validity_tls_prf (secret, sizeof (secret), seed, sizeof (seed), - output1, sizeof (output1)); - validity_tls_prf (secret, sizeof (secret), seed, sizeof (seed), - output2, sizeof (output2)); - - g_assert_cmpmem (output1, sizeof (output1), output2, sizeof (output2)); -} - -/* ================================================================ - * Test: PRF with known TLS 1.2 test vector - * ================================================================ - * RFC 5246 does not define test vectors for SHA-256 PRF directly, - * but we verify our implementation against python-validity's output. - */ -static void -test_prf_output_length (void) -{ - guint8 secret[32]; - guint8 seed[64]; - guint8 output[0x120]; /* Same as key_block size */ - - memset (secret, 0xAB, sizeof (secret)); - memset (seed, 0xCD, sizeof (seed)); - - validity_tls_prf (secret, sizeof (secret), seed, sizeof (seed), - output, sizeof (output)); - - /* PRF output should not be all zeros */ - gboolean all_zero = TRUE; - for (gsize i = 0; i < sizeof (output); i++) - { - if (output[i] != 0) - { - all_zero = FALSE; - break; - } - } - g_assert_false (all_zero); -} - -/* ================================================================ - * Test: PRF with different lengths uses correct number of HMAC iters - * ================================================================ */ -static void -test_prf_short_output (void) -{ - guint8 secret[] = { 0x01 }; - guint8 seed[] = { 0x02 }; - guint8 output_short[16]; - guint8 output_long[48]; - - validity_tls_prf (secret, 1, seed, 1, output_short, sizeof (output_short)); - validity_tls_prf (secret, 1, seed, 1, output_long, sizeof (output_long)); - - /* First 16 bytes should match */ - g_assert_cmpmem (output_short, 16, output_long, 16); -} - -/* ================================================================ - * Test: Encrypt then decrypt roundtrip - * ================================================================ */ -static void -test_encrypt_decrypt_roundtrip (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - /* Set up encryption/decryption keys (same for roundtrip test) */ - memset (tls.encryption_key, 0x42, TLS_AES_KEY_SIZE); - memset (tls.decryption_key, 0x42, TLS_AES_KEY_SIZE); - - guint8 plaintext[] = "Hello, TLS! This is a test message for encryption."; - gsize pt_len = sizeof (plaintext); - - gsize enc_len; - guint8 *encrypted = validity_tls_encrypt (&tls, plaintext, pt_len, &enc_len); - g_assert_nonnull (encrypted); - g_assert_cmpuint (enc_len, >, pt_len); /* IV + padded ciphertext */ - - GError *error = NULL; - gsize dec_len; - guint8 *decrypted = validity_tls_decrypt (&tls, encrypted, enc_len, - &dec_len, &error); - g_assert_no_error (error); - g_assert_nonnull (decrypted); - g_assert_cmpmem (plaintext, pt_len, decrypted, dec_len); - - g_free (encrypted); - g_free (decrypted); - validity_tls_free (&tls); -} - -/* ================================================================ - * Test: Encrypt with block-aligned data - * ================================================================ */ -static void -test_encrypt_block_aligned (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - memset (tls.encryption_key, 0x55, TLS_AES_KEY_SIZE); - memset (tls.decryption_key, 0x55, TLS_AES_KEY_SIZE); - - /* 16 bytes = exactly one AES block */ - guint8 plaintext[16]; - memset (plaintext, 0xAA, 16); - - gsize enc_len; - guint8 *encrypted = validity_tls_encrypt (&tls, plaintext, 16, &enc_len); - g_assert_nonnull (encrypted); - /* Should be IV(16) + 32 bytes (16 data + 16 padding since pad=0x0f*16) */ - g_assert_cmpuint (enc_len, ==, 16 + 32); - - GError *error = NULL; - gsize dec_len; - guint8 *decrypted = validity_tls_decrypt (&tls, encrypted, enc_len, - &dec_len, &error); - g_assert_no_error (error); - g_assert_nonnull (decrypted); - g_assert_cmpuint (dec_len, ==, 16); - g_assert_cmpmem (plaintext, 16, decrypted, 16); - - g_free (encrypted); - g_free (decrypted); - validity_tls_free (&tls); -} - -/* ================================================================ - * Test: Decrypt with invalid data fails - * ================================================================ */ -static void -test_decrypt_invalid (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - memset (tls.decryption_key, 0x55, TLS_AES_KEY_SIZE); - - /* Too short for IV + block */ - guint8 short_data[10]; - memset (short_data, 0, sizeof (short_data)); - - GError *error = NULL; - gsize dec_len; - guint8 *decrypted = validity_tls_decrypt (&tls, short_data, - sizeof (short_data), - &dec_len, &error); - g_assert_null (decrypted); - g_assert_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_PROTO); - g_clear_error (&error); - - validity_tls_free (&tls); -} - -/* ================================================================ - * Test: PSK derivation runs without crashing - * ================================================================ */ -static void -test_psk_derivation (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - validity_tls_derive_psk (&tls); - - /* PSK keys should not be all zeros */ - gboolean all_zero = TRUE; - for (gsize i = 0; i < TLS_AES_KEY_SIZE; i++) - { - if (tls.psk_encryption_key[i] != 0) - { - all_zero = FALSE; - break; - } - } - g_assert_false (all_zero); - - all_zero = TRUE; - for (gsize i = 0; i < TLS_AES_KEY_SIZE; i++) - { - if (tls.psk_validation_key[i] != 0) - { - all_zero = FALSE; - break; - } - } - g_assert_false (all_zero); - - validity_tls_free (&tls); -} - -/* ================================================================ - * Test: PSK derivation is deterministic - * ================================================================ */ -static void -test_psk_deterministic (void) -{ - ValidityTlsState tls1, tls2; - - validity_tls_init (&tls1); - validity_tls_init (&tls2); - - validity_tls_derive_psk (&tls1); - validity_tls_derive_psk (&tls2); - - g_assert_cmpmem (tls1.psk_encryption_key, TLS_AES_KEY_SIZE, - tls2.psk_encryption_key, TLS_AES_KEY_SIZE); - g_assert_cmpmem (tls1.psk_validation_key, TLS_AES_KEY_SIZE, - tls2.psk_validation_key, TLS_AES_KEY_SIZE); - - validity_tls_free (&tls1); - validity_tls_free (&tls2); -} - -/* ================================================================ - * Test: Flash parse with empty data fails gracefully - * ================================================================ */ -static void -test_flash_parse_empty (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - GError *error = NULL; - guint8 empty_flash[] = { 0xFF, 0xFF, 0x00, 0x00 }; /* end block */ - - /* Flash with only end marker → missing keys */ - gboolean result = validity_tls_parse_flash (&tls, empty_flash, - sizeof (empty_flash), - &error); - g_assert_false (result); - g_assert_nonnull (error); - g_assert_false (tls.keys_loaded); - - g_clear_error (&error); - validity_tls_free (&tls); -} - -/* ================================================================ - * Test: Flash parse with truncated data fails gracefully - * ================================================================ */ -static void -test_flash_parse_truncated (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - GError *error = NULL; - guint8 truncated[] = { 0x03, 0x00, 0xFF, 0x00 }; /* cert block w/ impossibly large size */ - - gboolean result = validity_tls_parse_flash (&tls, truncated, - sizeof (truncated), - &error); - /* Should fail due to block size exceeding remaining data */ - g_assert_false (result); - g_assert_nonnull (error); - g_clear_error (&error); - - validity_tls_free (&tls); -} - -/* ================================================================ - * Test: Init/free cycle doesn't leak - * ================================================================ */ -static void -test_init_free (void) -{ - ValidityTlsState tls; - - for (int i = 0; i < 10; i++) - { - validity_tls_init (&tls); - validity_tls_free (&tls); - } -} - -/* ================================================================ - * Test: Build ClientHello produces valid TLS record - * ================================================================ */ -static void -test_build_client_hello (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - gsize out_len; - guint8 *hello = validity_tls_build_client_hello (&tls, &out_len); - - g_assert_nonnull (hello); - g_assert_cmpuint (out_len, >, 4 + 5); /* prefix(4) + record header(5) minimum */ - - /* Check prefix: 0x44 0x00 0x00 0x00 */ - g_assert_cmpint (hello[0], ==, 0x44); - g_assert_cmpint (hello[1], ==, 0x00); - g_assert_cmpint (hello[2], ==, 0x00); - g_assert_cmpint (hello[3], ==, 0x00); - - /* Check TLS record header */ - g_assert_cmpint (hello[4], ==, 0x16); /* handshake */ - g_assert_cmpint (hello[5], ==, 0x03); /* version major */ - g_assert_cmpint (hello[6], ==, 0x03); /* version minor */ - - /* client_random should have been set */ - gboolean has_random = FALSE; - for (gsize i = 0; i < TLS_RANDOM_SIZE; i++) - { - if (tls.client_random[i] != 0) - { - has_random = TRUE; - break; - } - } - g_assert_true (has_random); - - g_free (hello); - validity_tls_free (&tls); -} - -/* ================================================================ - * Test: Wrap/unwrap with invalid data fails gracefully - * ================================================================ */ -static void -test_unwrap_invalid (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - GError *error = NULL; - gsize out_len; - - /* Short data → truncated record header */ - guint8 short_data[] = { 0x17, 0x03 }; - guint8 *result = validity_tls_unwrap_response (&tls, short_data, - sizeof (short_data), - &out_len, &error); - g_assert_null (result); - g_assert_nonnull (error); - g_clear_error (&error); - - /* App data before secure channel */ - guint8 app_early[] = { 0x17, 0x03, 0x03, 0x00, 0x10, - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; - result = validity_tls_unwrap_response (&tls, app_early, - sizeof (app_early), - &out_len, &error); - g_assert_null (result); - g_assert_nonnull (error); - g_clear_error (&error); - - validity_tls_free (&tls); -} - -/* ================================================================ - * Regression: Bug #1 — Flash parse requires PSK for private key - * - * Private key block (ID 4) is encrypted with PSK. Calling parse_flash - * without first deriving PSK must fail (HMAC mismatch), proving the - * ordering dependency. This catches the bug where flash_read SSM - * parsed flash data BEFORE PSK derivation had occurred. - * ================================================================ */ -static void -test_flash_parse_needs_psk (void) -{ - ValidityTlsState tls_with_psk, tls_no_psk; - - validity_tls_init (&tls_with_psk); - validity_tls_init (&tls_no_psk); - - /* Derive PSK so we can build a valid encrypted private key block */ - validity_tls_derive_psk (&tls_with_psk); - - /* Build a realistic flash image with a cert block + encrypted privkey block. - * We use a minimal cert (just 16 bytes of dummy data) and a privkey block - * that's encrypted with the proper PSK. */ - - /* Step 1: Build a cert body */ - guint8 cert_body[16]; - memset (cert_body, 0xAA, sizeof (cert_body)); - - /* Step 2: Build a private-key body encrypted with PSK */ - guint8 priv_plaintext[96]; /* d(32) + pad for block alignment */ - memset (priv_plaintext, 0xBB, sizeof (priv_plaintext)); - - /* Encrypt plaintext with PSK encryption key */ - guint8 iv[TLS_IV_SIZE]; - memset (iv, 0x11, TLS_IV_SIZE); - gsize ct_len = sizeof (priv_plaintext); - guint8 *ciphertext = g_malloc (TLS_IV_SIZE + ct_len); - memcpy (ciphertext, iv, TLS_IV_SIZE); - - EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new (); - int out_len, final_len; - EVP_EncryptInit_ex (ctx, EVP_aes_256_cbc (), NULL, - tls_with_psk.psk_encryption_key, iv); - EVP_CIPHER_CTX_set_padding (ctx, 0); - EVP_EncryptUpdate (ctx, ciphertext + TLS_IV_SIZE, &out_len, - priv_plaintext, ct_len); - EVP_EncryptFinal_ex (ctx, ciphertext + TLS_IV_SIZE + out_len, &final_len); - EVP_CIPHER_CTX_free (ctx); - gsize enc_total = TLS_IV_SIZE + ct_len; - - /* HMAC over (iv + ciphertext) with psk_validation_key */ - guint8 mac[TLS_HMAC_SIZE]; - unsigned int mac_len; - HMAC (EVP_sha256 (), - tls_with_psk.psk_validation_key, TLS_AES_KEY_SIZE, - ciphertext, enc_total, mac, &mac_len); - - /* Private key block payload: 0x02 || ciphertext || hmac */ - gsize priv_block_len = 1 + enc_total + TLS_HMAC_SIZE; - guint8 *priv_block = g_malloc (priv_block_len); - priv_block[0] = 0x02; - memcpy (priv_block + 1, ciphertext, enc_total); - memcpy (priv_block + 1 + enc_total, mac, TLS_HMAC_SIZE); - g_free (ciphertext); - - /* Build flash image: [cert_header][cert_body][priv_header][priv_body][end] */ - GByteArray *flash = g_byte_array_new (); - - /* Cert block header: id=0x0003, size, sha256 hash */ - guint8 cert_hdr[TLS_FLASH_BLOCK_HEADER_SIZE]; - FP_WRITE_UINT16_LE (cert_hdr, TLS_FLASH_BLOCK_CERT); - FP_WRITE_UINT16_LE (cert_hdr + 2, sizeof (cert_body)); - GChecksum *cs = g_checksum_new (G_CHECKSUM_SHA256); - gsize hash_len = 32; - g_checksum_update (cs, cert_body, sizeof (cert_body)); - g_checksum_get_digest (cs, cert_hdr + 4, &hash_len); - g_checksum_free (cs); - g_byte_array_append (flash, cert_hdr, sizeof (cert_hdr)); - g_byte_array_append (flash, cert_body, sizeof (cert_body)); - - /* Priv block header */ - guint8 priv_hdr[TLS_FLASH_BLOCK_HEADER_SIZE]; - FP_WRITE_UINT16_LE (priv_hdr, TLS_FLASH_BLOCK_PRIVKEY); - FP_WRITE_UINT16_LE (priv_hdr + 2, priv_block_len); - cs = g_checksum_new (G_CHECKSUM_SHA256); - hash_len = 32; - g_checksum_update (cs, priv_block, priv_block_len); - g_checksum_get_digest (cs, priv_hdr + 4, &hash_len); - g_checksum_free (cs); - g_byte_array_append (flash, priv_hdr, sizeof (priv_hdr)); - g_byte_array_append (flash, priv_block, priv_block_len); - - /* End marker */ - guint8 end_marker[4] = { 0xFF, 0xFF, 0x00, 0x00 }; - g_byte_array_append (flash, end_marker, sizeof (end_marker)); - - /* TEST: Without PSK, parse_flash must fail on the privkey block */ - GError *error = NULL; - gboolean result = validity_tls_parse_flash (&tls_no_psk, - flash->data, flash->len, - &error); - g_assert_false (result); - g_assert_nonnull (error); - /* Should fail with HMAC-related error since PSK is all zeros */ - g_clear_error (&error); - - g_byte_array_free (flash, TRUE); - g_free (priv_block); - validity_tls_free (&tls_with_psk); - validity_tls_free (&tls_no_psk); -} - -/* ================================================================ - * Regression: Bug #2 — READ_FLASH command format - * - * The READ_FLASH command must be exactly 13 bytes matching - * python-validity: pack('message, "TLS flash: incomplete key data")); - g_clear_error (&error); - validity_tls_free (&tls); - - /* Verify the bug scenario: passing the raw response (with the 6-byte - * header) gives DIFFERENT data to the parser than the correctly unwrapped - * payload. The first 4 bytes of the raw response are the LE size field - * (0x04 0x00 0x00 0x00), which would be misinterpreted as block_id=0x0004 - * (PRIVKEY block with size 0). This is a data corruption — the parser - * receives wrong input either way, but the key point is that the raw - * response and the unwrapped payload are NOT the same buffer content. */ - g_assert_cmpuint (sizeof (response), !=, payload_len); - g_assert_true (memcmp (response, payload, payload_len) != 0); -} - -/* ================================================================ - * Regression: Bug #4 — TLS handshake expects raw TLS records - * - * parse_server_hello expects raw TLS records starting with a content - * type byte (0x16 for Handshake). The old code used vcsfw_cmd_send - * which strips 2 bytes of VCSFW status, corrupting the TLS record. - * This test verifies that: - * - A valid TLS Handshake record header is accepted - * - Data prefixed with a 2-byte VCSFW status is rejected - * ================================================================ */ -static void -test_server_hello_rejects_vcsfw_prefix (void) -{ - /* Build a minimal valid TLS ServerHello record */ - guint8 server_hello_msg[] = { - /* Handshake message: ServerHello (type 0x02) */ - 0x02, /* type: ServerHello */ - 0x00, 0x00, 0x26, /* length: 38 bytes */ - 0x03, 0x03, /* version 1.2 */ - /* 32 bytes server_random */ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, - 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, - 0x00, /* session_id length: 0 */ - 0xC0, 0x05, /* cipher suite: 0xC005 */ - 0x00, /* compression: none */ - }; - - gsize hs_len = sizeof (server_hello_msg); - - /* Wrap in TLS record: content_type(1) + version(2) + length(2) + body */ - gsize raw_tls_len = 5 + hs_len; - guint8 *raw_tls = g_malloc (raw_tls_len); - - raw_tls[0] = TLS_CONTENT_HANDSHAKE; /* 0x16 */ - raw_tls[1] = TLS_VERSION_MAJOR; - raw_tls[2] = TLS_VERSION_MINOR; - raw_tls[3] = (hs_len >> 8) & 0xff; - raw_tls[4] = hs_len & 0xff; - memcpy (raw_tls + 5, server_hello_msg, hs_len); - - /* Test 1: parse_server_hello with raw TLS — should succeed */ - ValidityTlsState tls; - validity_tls_init (&tls); - tls.handshake_hash = g_checksum_new (G_CHECKSUM_SHA256); - GError *error = NULL; - - gboolean result = validity_tls_parse_server_hello (&tls, raw_tls, - raw_tls_len, &error); - g_assert_no_error (error); - g_assert_true (result); - /* Verify server_random was properly extracted */ - g_assert_cmpint (tls.server_random[0], ==, 0x01); - g_assert_cmpint (tls.server_random[31], ==, 0x20); - validity_tls_free (&tls); - - /* Test 2: Prepend a 2-byte VCSFW status (0x0000) — simulates what - * vcsfw_cmd_send's cmd_receive_cb would have already STRIPPED. - * But if the raw recv path is wrong and doesn't strip, the parser - * gets [0x00, 0x00, 0x16, ...] — first byte 0x00 is not a valid - * TLS content type, so parsing should behave differently. */ - gsize prefixed_len = 2 + raw_tls_len; - guint8 *prefixed = g_malloc (prefixed_len); - prefixed[0] = 0x00; /* VCSFW status lo */ - prefixed[1] = 0x00; /* VCSFW status hi */ - memcpy (prefixed + 2, raw_tls, raw_tls_len); - - validity_tls_init (&tls); - tls.handshake_hash = g_checksum_new (G_CHECKSUM_SHA256); - - result = validity_tls_parse_server_hello (&tls, prefixed, prefixed_len, - &error); - /* With the 2-byte prefix, the first "record" starts at byte 0: - * content_type=0x00 is NOT TLS_CONTENT_HANDSHAKE (0x16), so the - * parser treats it as unknown content and either fails or skips it, - * and the server_random will NOT match the expected values. */ - if (result) - { - /* Even if parsing didn't error, server_random should be wrong */ - gboolean random_ok = (tls.server_random[0] == 0x01 && - tls.server_random[31] == 0x20); - g_assert_false (random_ok); - } - g_clear_error (&error); - validity_tls_free (&tls); - - g_free (raw_tls); - g_free (prefixed); -} - -/* ================================================================ - * Regression: Bug #5 — Client hello has 0x44 prefix (not VCSFW cmd) - * - * TLS handshake messages use 0x44000000 as a 4-byte prefix, NOT a - * standard VCSFW command byte. This test verifies the prefix and that - * the TLS record immediately follows (no VCSFW status expected in - * response). - * ================================================================ */ -static void -test_client_hello_tls_prefix (void) -{ - ValidityTlsState tls; - - validity_tls_init (&tls); - - gsize out_len; - guint8 *hello = validity_tls_build_client_hello (&tls, &out_len); - g_assert_nonnull (hello); - - /* Must start with 0x44 0x00 0x00 0x00 (TLS prefix, not VCSFW) */ - g_assert_cmpint (hello[0], ==, 0x44); - g_assert_cmpint (hello[1], ==, 0x00); - g_assert_cmpint (hello[2], ==, 0x00); - g_assert_cmpint (hello[3], ==, 0x00); - - /* Byte 4 must be TLS Handshake content type (0x16) */ - g_assert_cmpint (hello[4], ==, TLS_CONTENT_HANDSHAKE); - - /* Bytes 5-6 must be TLS version 1.2 (0x0303) */ - g_assert_cmpint (hello[5], ==, TLS_VERSION_MAJOR); - g_assert_cmpint (hello[6], ==, TLS_VERSION_MINOR); - - /* The prefix (0x44) must NOT equal any VCSFW command byte. - * Specifically, 0x44 != VCSFW_CMD_READ_FLASH (0x40) and - * is not any known VCSFW command. This proves TLS messages - * travel on a separate "channel". */ - g_assert_cmpint (hello[0], !=, VCSFW_CMD_GET_VERSION); - g_assert_cmpint (hello[0], !=, VCSFW_CMD_READ_FLASH); - g_assert_cmpint (hello[0], !=, VCSFW_CMD_GET_FW_INFO); - - g_free (hello); - validity_tls_free (&tls); -} - -/* ================================================================ - * Main - * ================================================================ */ - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - g_test_add_func ("/validity/tls/prf/deterministic", test_prf_deterministic); - g_test_add_func ("/validity/tls/prf/output-length", test_prf_output_length); - g_test_add_func ("/validity/tls/prf/short-output", test_prf_short_output); - g_test_add_func ("/validity/tls/encrypt/roundtrip", - test_encrypt_decrypt_roundtrip); - g_test_add_func ("/validity/tls/encrypt/block-aligned", - test_encrypt_block_aligned); - g_test_add_func ("/validity/tls/decrypt/invalid", test_decrypt_invalid); - g_test_add_func ("/validity/tls/psk/derivation", test_psk_derivation); - g_test_add_func ("/validity/tls/psk/deterministic", test_psk_deterministic); - g_test_add_func ("/validity/tls/flash/parse-empty", test_flash_parse_empty); - g_test_add_func ("/validity/tls/flash/parse-truncated", - test_flash_parse_truncated); - g_test_add_func ("/validity/tls/init-free", test_init_free); - g_test_add_func ("/validity/tls/client-hello", test_build_client_hello); - g_test_add_func ("/validity/tls/unwrap/invalid", test_unwrap_invalid); - - /* Regression tests for hardware-discovered bugs */ - g_test_add_func ("/validity/tls/regression/flash-parse-needs-psk", - test_flash_parse_needs_psk); - g_test_add_func ("/validity/tls/regression/flash-cmd-format", - test_flash_cmd_format); - g_test_add_func ("/validity/tls/regression/flash-response-header", - test_flash_response_header); - g_test_add_func ("/validity/tls/regression/server-hello-rejects-vcsfw-prefix", - test_server_hello_rejects_vcsfw_prefix); - g_test_add_func ("/validity/tls/regression/client-hello-tls-prefix", - test_client_hello_tls_prefix); - - return g_test_run (); -} diff --git a/tests/test-validity-verify.c b/tests/test-validity-verify.c deleted file mode 100644 index 01bc3294..00000000 --- a/tests/test-validity-verify.c +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Regression tests for validity verify/identify/delete/clear operations. - * - * These tests cover issues found during the Iteration 6 code audit: - * 1. parse_match_result dead while loop — TLV dict not iterated - * 2. ENROLL_CREATE_USER NULL user_id — validity_db_build_identity(NULL) crash - * 3. Identify always returns first gallery print — subtype not matched - * 4. delete_storage_dbid field reused for enrollment — struct field abuse - * 5. Delete SSM non-functional — del_record never called - * 6. match_finger double allocation — 12 bytes freed then 13 allocated - * 7. clear_storage returned NOT_SUPPORTED - * 8. Stale TODOs - * - * Copyright (C) 2024 libfprint contributors - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - */ - -#include -#include - -#include "fp-enums.h" -#include "fpi-device.h" -#include "fpi-byte-utils.h" -#include "fp-print.h" -#include "test-device-fake.h" - -#include "drivers/validity/validity.h" -#include "drivers/validity/validity_db.h" -#include "drivers/validity/validity_capture.h" -#include "drivers/validity/vcsfw_protocol.h" - -/* ================================================================ - * Helper: build a TLV dict entry tag(2LE) | len(2LE) | data[len] - * Returns bytes written. - * ================================================================ */ -static gsize -build_tlv_entry (guint8 *buf, guint16 tag, const guint8 *data, guint16 len) -{ - FP_WRITE_UINT16_LE (&buf[0], tag); - FP_WRITE_UINT16_LE (&buf[2], len); - if (len > 0) - memcpy (&buf[4], data, len); - return 4 + len; -} - -/* ================================================================ - * Helper: Build a complete match result payload: - * total_len(2LE) | TLV entries... - * ================================================================ */ -static guint8 * -build_match_payload (guint32 user_dbid, - guint16 subtype, - const guint8 *hash, - gsize hash_len, - gsize *out_len) -{ - /* Max size: 2 (total_len) + 3 entries × (4 header + max data) */ - guint8 *buf = g_new0 (guint8, 256); - gsize pos = 2; /* skip total_len placeholder */ - - /* Tag 1: user_dbid (4 bytes LE) */ - guint8 dbid_data[4]; - - FP_WRITE_UINT32_LE (dbid_data, user_dbid); - pos += build_tlv_entry (&buf[pos], 1, dbid_data, 4); - - /* Tag 3: subtype (2 bytes LE) */ - guint8 sub_data[2]; - FP_WRITE_UINT16_LE (sub_data, subtype); - pos += build_tlv_entry (&buf[pos], 3, sub_data, 2); - - /* Tag 4: hash */ - if (hash && hash_len > 0) - pos += build_tlv_entry (&buf[pos], 4, hash, hash_len); - - /* Write total_len at offset 0 */ - FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); - - *out_len = pos; - return buf; -} - -/* ================================================================ - * R1: parse_match_result with valid TLV data - * - * Regression: Issue #1 — dead while loop would never extract fields. - * Verifies that user_dbid, subtype, and hash are correctly parsed - * from a TLV dictionary matching python-validity's parse_dict() format. - * ================================================================ */ -static void -test_parse_match_result_valid (void) -{ - guint8 hash[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; - gsize payload_len; - g_autofree guint8 *payload = build_match_payload ( - 0x00001234, 3, hash, sizeof (hash), &payload_len); - - ValidityMatchResult result = { 0 }; - gboolean ok = validity_parse_match_result (payload, payload_len, &result); - - g_assert_true (ok); - g_assert_true (result.matched); - g_assert_cmpuint (result.user_dbid, ==, 0x00001234); - g_assert_cmpuint (result.subtype, ==, 3); - g_assert_nonnull (result.hash); - g_assert_cmpuint (result.hash_len, ==, sizeof (hash)); - g_assert_cmpmem (result.hash, result.hash_len, hash, sizeof (hash)); - - validity_match_result_clear (&result); -} - -/* ================================================================ - * R1b: parse_match_result iterates ALL TLV entries - * - * Regression: The dead while loop would break after first entry. - * Build a dict with tag 3 (subtype) BEFORE tag 1 (user_dbid) to - * ensure the parser doesn't stop after the first entry. - * ================================================================ */ -static void -test_parse_match_result_multi_tags (void) -{ - /* Manually build: total_len(2) | tag3(2+2+2) | tag1(2+2+4) | tag4(2+2+3) */ - guint8 buf[256]; - gsize pos = 2; - - /* Tag 3 first: subtype = 7 */ - guint8 sub[2]; - - FP_WRITE_UINT16_LE (sub, 7); - pos += build_tlv_entry (&buf[pos], 3, sub, 2); - - /* Tag 1 second: user_dbid = 0xDEADBEEF */ - guint8 dbid[4]; - FP_WRITE_UINT32_LE (dbid, 0xDEADBEEF); - pos += build_tlv_entry (&buf[pos], 1, dbid, 4); - - /* Tag 4 third: hash = {0x11, 0x22, 0x33} */ - guint8 hash[] = { 0x11, 0x22, 0x33 }; - pos += build_tlv_entry (&buf[pos], 4, hash, 3); - - FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); - - ValidityMatchResult result = { 0 }; - gboolean ok = validity_parse_match_result (buf, pos, &result); - - g_assert_true (ok); - g_assert_true (result.matched); - g_assert_cmpuint (result.user_dbid, ==, 0xDEADBEEF); - g_assert_cmpuint (result.subtype, ==, 7); - g_assert_nonnull (result.hash); - g_assert_cmpuint (result.hash_len, ==, 3); - g_assert_cmpmem (result.hash, result.hash_len, hash, 3); - - validity_match_result_clear (&result); -} - -/* ================================================================ - * R1c: parse_match_result with empty dict (no match) - * - * Regression: A no-match scenario should return ok=TRUE but matched=FALSE. - * ================================================================ */ -static void -test_parse_match_result_empty (void) -{ - /* total_len = 0, no TLV entries */ - guint8 buf[2] = { 0x00, 0x00 }; - - ValidityMatchResult result = { 0 }; - gboolean ok = validity_parse_match_result (buf, sizeof (buf), &result); - - g_assert_true (ok); - g_assert_false (result.matched); - g_assert_cmpuint (result.user_dbid, ==, 0); - g_assert_cmpuint (result.subtype, ==, 0); - g_assert_null (result.hash); -} - -/* ================================================================ - * R1d: parse_match_result with truncated data - * - * Ensure graceful handling of malformed/truncated payloads. - * ================================================================ */ -static void -test_parse_match_result_truncated (void) -{ - /* Only 1 byte — too short for total_len */ - guint8 buf1[1] = { 0x05 }; - ValidityMatchResult result = { 0 }; - - g_assert_false (validity_parse_match_result (buf1, 1, &result)); - - /* total_len says 20 but only 6 bytes follow (partial TLV entry) */ - guint8 buf2[8]; - FP_WRITE_UINT16_LE (&buf2[0], 20); - FP_WRITE_UINT16_LE (&buf2[2], 1); /* tag = 1 */ - FP_WRITE_UINT16_LE (&buf2[4], 10); /* len = 10, but only 2 bytes remain */ - buf2[6] = 0xFF; - buf2[7] = 0xFF; - - memset (&result, 0, sizeof (result)); - gboolean ok = validity_parse_match_result (buf2, sizeof (buf2), &result); - /* Should return TRUE (parsing succeeded) but matched=FALSE (incomplete entry) */ - g_assert_true (ok); - g_assert_false (result.matched); -} - -/* ================================================================ - * R1e: parse_match_result ignores unknown tags - * - * Unknown tags should be skipped without error. - * ================================================================ */ -static void -test_parse_match_result_unknown_tags (void) -{ - guint8 buf[256]; - gsize pos = 2; - - /* Unknown tag 99 with 2 bytes of data */ - guint8 unk[] = { 0x42, 0x43 }; - - pos += build_tlv_entry (&buf[pos], 99, unk, 2); - - /* Tag 1: user_dbid = 0x0042 */ - guint8 dbid[4]; - FP_WRITE_UINT32_LE (dbid, 0x0042); - pos += build_tlv_entry (&buf[pos], 1, dbid, 4); - - FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); - - ValidityMatchResult result = { 0 }; - gboolean ok = validity_parse_match_result (buf, pos, &result); - - g_assert_true (ok); - g_assert_true (result.matched); - g_assert_cmpuint (result.user_dbid, ==, 0x0042); - - validity_match_result_clear (&result); -} - -/* ================================================================ - * R2: validity_db_build_identity rejects NULL - * - * Regression: Issue #2 — NULL user_id was passed to build_identity - * which would then be passed to g_variant_new_string(NULL) → crash. - * The guard should return NULL. - * ================================================================ */ -static void -test_build_identity_null (void) -{ - gsize len = 999; - - /* The function uses g_return_val_if_fail which emits g_critical. - * With G_DEBUG=fatal-warnings the critical would be fatal, - * so we must expect the message. */ - g_test_expect_message ("libfprint-validity", - G_LOG_LEVEL_CRITICAL, - "*assertion*uuid_str*failed*"); - - guint8 *id = validity_db_build_identity (NULL, &len); - g_test_assert_expected_messages (); - - g_assert_null (id); -} - -/* ================================================================ - * R2b: validity_db_build_identity with valid UUID - * - * Regression: Ensures UUID → identity bytes works correctly - * (complementary to the NULL test above). - * ================================================================ */ -static void -test_build_identity_valid_uuid (void) -{ - const gchar *uuid = "12345678-1234-5678-1234-567812345678"; - gsize len; - g_autofree guint8 *id = validity_db_build_identity (uuid, &len); - - g_assert_nonnull (id); - g_assert_cmpuint (len, >=, VALIDITY_IDENTITY_MIN_SIZE); - - /* Type should be SID (3) */ - g_assert_cmpuint (FP_READ_UINT32_LE (&id[0]), ==, VALIDITY_IDENTITY_TYPE_SID); - - /* Length field should be UUID string length */ - g_assert_cmpuint (FP_READ_UINT32_LE (&id[4]), ==, strlen (uuid)); - - /* UUID payload should be present */ - g_assert_cmpmem (&id[8], strlen (uuid), uuid, strlen (uuid)); -} - -/* ================================================================ - * R3: Gallery matching by subtype - * - * Regression: Issue #3 — identify always returned first gallery print - * regardless of actual subtype. Now it should match by finger subtype. - * ================================================================ */ -static void -test_gallery_match_by_subtype (void) -{ - g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); - g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); - - /* Create 3 prints with fingers: LEFT_THUMB(1), LEFT_INDEX(2), RIGHT_MIDDLE(8) */ - FpPrint *p1 = fp_print_new (device); - fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); - g_ptr_array_add (gallery, g_object_ref_sink (p1)); - - FpPrint *p2 = fp_print_new (device); - fp_print_set_finger (p2, FP_FINGER_LEFT_INDEX); - g_ptr_array_add (gallery, g_object_ref_sink (p2)); - - FpPrint *p3 = fp_print_new (device); - fp_print_set_finger (p3, FP_FINGER_RIGHT_MIDDLE); - g_ptr_array_add (gallery, g_object_ref_sink (p3)); - - /* Subtype 2 = LEFT_INDEX → should match p2, not p1 */ - guint16 subtype_left_index = validity_finger_to_subtype (FP_FINGER_LEFT_INDEX); - FpPrint *match = validity_find_gallery_match (gallery, subtype_left_index); - g_assert_true (match == p2); - - /* Subtype 8 = RIGHT_MIDDLE → should match p3 */ - guint16 subtype_right_middle = validity_finger_to_subtype (FP_FINGER_RIGHT_MIDDLE); - match = validity_find_gallery_match (gallery, subtype_right_middle); - g_assert_true (match == p3); - - /* Subtype 1 = LEFT_THUMB → should match p1 */ - guint16 subtype_left_thumb = validity_finger_to_subtype (FP_FINGER_LEFT_THUMB); - match = validity_find_gallery_match (gallery, subtype_left_thumb); - g_assert_true (match == p1); -} - -/* ================================================================ - * R3b: Gallery match falls back to first when subtype doesn't match - * - * The sensor confirmed a match but the subtype can't be correlated - * to any gallery entry — should fall back to first. - * ================================================================ */ -static void -test_gallery_match_fallback (void) -{ - g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); - g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); - - FpPrint *p1 = fp_print_new (device); - fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); - g_ptr_array_add (gallery, g_object_ref_sink (p1)); - - /* Subtype 9 = RIGHT_RING, not in gallery → should fall back to p1 */ - guint16 subtype_right_ring = validity_finger_to_subtype (FP_FINGER_RIGHT_RING); - FpPrint *match = validity_find_gallery_match (gallery, subtype_right_ring); - g_assert_true (match == p1); -} - -/* ================================================================ - * R3c: Gallery match with NULL/empty gallery - * - * Should return NULL when gallery is empty or NULL. - * ================================================================ */ -static void -test_gallery_match_empty (void) -{ - g_autoptr(GPtrArray) empty = g_ptr_array_new_with_free_func (g_object_unref); - - g_assert_null (validity_find_gallery_match (NULL, 1)); - g_assert_null (validity_find_gallery_match (empty, 1)); -} - -/* ================================================================ - * R4: enroll_user_dbid field exists separately from delete_storage_dbid - * - * Regression: Issue #4 — delete_storage_dbid was abused for enrollment. - * This compile-time test verifies both fields exist independently. - * ================================================================ */ -static void -test_struct_separate_fields (void) -{ - /* Verify both fields exist and are at different offsets */ - g_assert_cmpuint ( - G_STRUCT_OFFSET (FpiDeviceValidity, enroll_user_dbid), !=, - G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); - - /* Also verify delete_finger_subtype and delete_finger_dbid exist - * (needed for the functional delete SSM) */ - g_assert_cmpuint ( - G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_subtype), !=, - G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); - g_assert_cmpuint ( - G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_dbid), !=, - G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); -} - -/* ================================================================ - * R5: del_record command format - * - * Regression: Issue #5 — delete SSM never sent del_record cmd. - * Verify cmd 0x48 produces correct format: 0x48 | dbid(2LE). - * (This already exists in test-validity-db.c but we double-check - * the format critical for delete functionality.) - * ================================================================ */ -static void -test_del_record_format (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_del_record (0x4321, &len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 3); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DEL_RECORD); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x4321); -} - -/* ================================================================ - * R6: match_finger command is exactly 13 bytes (single allocation) - * - * Regression: Issue #6 — build_cmd_match_finger allocated 12 bytes, - * freed, then re-allocated 13 bytes. Now single allocation. - * ================================================================ */ -static void -test_match_finger_size (void) -{ - gsize len; - g_autofree guint8 *cmd = validity_db_build_cmd_match_finger (&len); - - g_assert_nonnull (cmd); - g_assert_cmpuint (len, ==, 13); - g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_FINGER); - g_assert_cmpuint (cmd[1], ==, 0x02); - g_assert_cmpuint (cmd[2], ==, 0xFF); - - /* Verify all 5 uint16_le fields */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); /* stg_id */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); /* usr_id */ - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, 1); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[9]), ==, 0); - g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[11]), ==, 0); -} - -/* ================================================================ - * R7: Clear storage SSM states exist - * - * Regression: Issue #7 — clear_storage was a stub returning NOT_SUPPORTED. - * Verify the CLEAR_* enum states exist (compile-time regression test). - * ================================================================ */ -static void -test_clear_storage_states_exist (void) -{ - /* Verify clear SSM states exist and are ordered correctly */ - g_assert_cmpint (CLEAR_GET_STORAGE, ==, 0); - g_assert_cmpint (CLEAR_GET_STORAGE_RECV, ==, 1); - g_assert_cmpint (CLEAR_DEL_USER, ==, 2); - g_assert_cmpint (CLEAR_DEL_USER_RECV, ==, 3); - g_assert_cmpint (CLEAR_DONE, ==, 4); - g_assert_cmpint (CLEAR_NUM_STATES, ==, 5); -} - -/* ================================================================ - * R7b: Delete SSM states are complete - * - * Verify the delete SSM has all required states including - * DEL_RECORD and DEL_RECORD_RECV (which were previously dead code). - * ================================================================ */ -static void -test_delete_states_exist (void) -{ - g_assert_cmpint (DELETE_GET_STORAGE, ==, 0); - g_assert_cmpint (DELETE_GET_STORAGE_RECV, ==, 1); - g_assert_cmpint (DELETE_LOOKUP_USER, ==, 2); - g_assert_cmpint (DELETE_LOOKUP_USER_RECV, ==, 3); - g_assert_cmpint (DELETE_DEL_RECORD, ==, 4); - g_assert_cmpint (DELETE_DEL_RECORD_RECV, ==, 5); - g_assert_cmpint (DELETE_DONE, ==, 6); - g_assert_cmpint (DELETE_NUM_STATES, ==, 7); -} - -/* ================================================================ - * R1f: match_result_clear frees hash - * - * Ensure the clear function properly frees the hash allocation. - * ================================================================ */ -static void -test_match_result_clear (void) -{ - ValidityMatchResult result = { 0 }; - - result.matched = TRUE; - result.user_dbid = 42; - result.subtype = 5; - result.hash = g_memdup2 ((guint8[]){0x01, 0x02}, 2); - result.hash_len = 2; - - validity_match_result_clear (&result); - - g_assert_false (result.matched); - g_assert_cmpuint (result.user_dbid, ==, 0); - g_assert_cmpuint (result.subtype, ==, 0); - g_assert_null (result.hash); - g_assert_cmpuint (result.hash_len, ==, 0); -} - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - /* R1: parse_match_result regression tests (Issue #1: dead while loop) */ - g_test_add_func ("/validity/verify/parse_match_result_valid", - test_parse_match_result_valid); - g_test_add_func ("/validity/verify/parse_match_result_multi_tags", - test_parse_match_result_multi_tags); - g_test_add_func ("/validity/verify/parse_match_result_empty", - test_parse_match_result_empty); - g_test_add_func ("/validity/verify/parse_match_result_truncated", - test_parse_match_result_truncated); - g_test_add_func ("/validity/verify/parse_match_result_unknown_tags", - test_parse_match_result_unknown_tags); - g_test_add_func ("/validity/verify/match_result_clear", - test_match_result_clear); - - /* R2: identity builder NULL regression (Issue #2: NULL crash) */ - g_test_add_func ("/validity/verify/build_identity_null", - test_build_identity_null); - g_test_add_func ("/validity/verify/build_identity_valid_uuid", - test_build_identity_valid_uuid); - - /* R3: gallery matching by subtype (Issue #3: always returned first) */ - g_test_add_func ("/validity/verify/gallery_match_by_subtype", - test_gallery_match_by_subtype); - g_test_add_func ("/validity/verify/gallery_match_fallback", - test_gallery_match_fallback); - g_test_add_func ("/validity/verify/gallery_match_empty", - test_gallery_match_empty); - - /* R4: struct field separation (Issue #4: field abuse) */ - g_test_add_func ("/validity/verify/struct_separate_fields", - test_struct_separate_fields); - - /* R5: del_record command format (Issue #5: delete SSM non-functional) */ - g_test_add_func ("/validity/verify/del_record_format", - test_del_record_format); - - /* R6: match_finger single allocation (Issue #6: double alloc) */ - g_test_add_func ("/validity/verify/match_finger_size", - test_match_finger_size); - - /* R7: clear/delete storage SSM states (Issue #7: stub) */ - g_test_add_func ("/validity/verify/clear_storage_states", - test_clear_storage_states_exist); - g_test_add_func ("/validity/verify/delete_states", - test_delete_states_exist); - - return g_test_run (); -} diff --git a/tests/test-validity.c b/tests/test-validity.c new file mode 100644 index 00000000..7cb8ddd1 --- /dev/null +++ b/tests/test-validity.c @@ -0,0 +1,5079 @@ +/* + * Unit tests for the validity (VCSFW) fingerprint driver + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "fpi-byte-utils.h" +#include "fpi-device.h" +#include "fpi-ssm.h" +#include "fp-enums.h" +#include "fp-print.h" +#include "fpi-byte-reader.h" + +#include "drivers/validity/validity.h" +#include "drivers/validity/validity_hal.h" +#include "drivers/validity/validity_sensor.h" +#include "drivers/validity/validity_fwext.h" +#include "drivers/validity/vcsfw_protocol.h" +#include "drivers/validity/validity_db.h" +#include "drivers/validity/validity_capture.h" +#include "drivers/validity/validity_tls.h" +#include "drivers/validity/validity_pair.h" + +#include "test-device-fake.h" + +/* ================================================================ + * Tests: HAL + * ================================================================ */ + + +/* ================================================================ + * T7.1: HAL lookup by device type — all valid types return non-NULL + * ================================================================ */ +static void +test_hal_lookup_all_types (void) +{ + const guint types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, + VALIDITY_DEV_9A, VALIDITY_DEV_9D }; + + for (guint i = 0; i < G_N_ELEMENTS (types); i++) + { + const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); + g_assert_nonnull (desc); + g_assert_cmpuint (desc->vid, >, 0); + g_assert_cmpuint (desc->pid, >, 0); + } +} + +/* ================================================================ + * T7.2: HAL lookup by PID — all supported VID/PID combos + * ================================================================ */ +static void +test_hal_lookup_by_pid (void) +{ + /* All 4 supported devices */ + struct { guint16 vid; + guint16 pid; + } devices[] = { + { 0x138a, 0x0090 }, + { 0x138a, 0x0097 }, + { 0x06cb, 0x009a }, + { 0x138a, 0x009d }, + }; + + for (guint i = 0; i < G_N_ELEMENTS (devices); i++) + { + const ValidityDeviceDesc *desc = + validity_hal_device_lookup_by_pid (devices[i].vid, devices[i].pid); + g_assert_nonnull (desc); + g_assert_cmpuint (desc->vid, ==, devices[i].vid); + g_assert_cmpuint (desc->pid, ==, devices[i].pid); + } +} + +/* ================================================================ + * T7.3: HAL lookup — invalid type returns NULL + * ================================================================ */ +static void +test_hal_lookup_invalid (void) +{ + const ValidityDeviceDesc *desc = validity_hal_device_lookup (99); + + g_assert_null (desc); +} + +/* ================================================================ + * T7.4: HAL lookup by PID — unknown PID returns NULL + * ================================================================ */ +static void +test_hal_lookup_by_pid_invalid (void) +{ + const ValidityDeviceDesc *desc = + validity_hal_device_lookup_by_pid (0x1234, 0x5678); + + g_assert_null (desc); +} + +/* ================================================================ + * T7.5: All devices have non-empty blobs + * ================================================================ */ +static void +test_hal_blobs_present (void) +{ + const guint types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, + VALIDITY_DEV_9A, VALIDITY_DEV_9D }; + + for (guint i = 0; i < G_N_ELEMENTS (types); i++) + { + const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); + g_assert_nonnull (desc); + + /* init_hardcoded must be present for all */ + g_assert_nonnull (desc->init_hardcoded); + g_assert_cmpuint (desc->init_hardcoded_len, >, 0); + + /* reset_blob must be present for all */ + g_assert_nonnull (desc->reset_blob); + g_assert_cmpuint (desc->reset_blob_len, >, 0); + + /* db_write_enable must be present for all */ + g_assert_nonnull (desc->db_write_enable); + g_assert_cmpuint (desc->db_write_enable_len, >, 0); + } +} + +/* ================================================================ + * T7.6: PID 0090 has smaller db partition and no clean_slate blob + * ================================================================ */ +static void +test_hal_pid_0090_specifics (void) +{ + const ValidityDeviceDesc *desc = validity_hal_device_lookup (VALIDITY_DEV_90); + + g_assert_nonnull (desc); + + /* 0090 has no init_hardcoded_clean_slate */ + g_assert_null (desc->init_clean_slate); + g_assert_cmpuint (desc->init_clean_slate_len, ==, 0); + + /* Flash layout should exist */ + g_assert_nonnull (desc->flash_layout); + g_assert_cmpuint (desc->flash_layout->num_partitions, ==, + VALIDITY_FLASH_NUM_PARTITIONS); +} + +/* ================================================================ + * T7.7: Non-0090 devices have init_hardcoded_clean_slate + * ================================================================ */ +static void +test_hal_clean_slate_present (void) +{ + const guint types[] = { VALIDITY_DEV_97, VALIDITY_DEV_9A, VALIDITY_DEV_9D }; + + for (guint i = 0; i < G_N_ELEMENTS (types); i++) + { + const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); + g_assert_nonnull (desc); + g_assert_nonnull (desc->init_clean_slate); + g_assert_cmpuint (desc->init_clean_slate_len, >, 0); + } +} + +/* ================================================================ + * T7.8: Flash layout has valid partition table + * ================================================================ */ +static void +test_hal_flash_layout (void) +{ + const guint types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, + VALIDITY_DEV_9A, VALIDITY_DEV_9D }; + + for (guint i = 0; i < G_N_ELEMENTS (types); i++) + { + const ValidityDeviceDesc *desc = validity_hal_device_lookup (types[i]); + g_assert_nonnull (desc); + g_assert_nonnull (desc->flash_layout); + + const ValidityFlashLayout *layout = desc->flash_layout; + g_assert_cmpuint (layout->num_partitions, ==, + VALIDITY_FLASH_NUM_PARTITIONS); + + /* Signature must be 256 bytes */ + g_assert_nonnull (layout->partition_sig); + g_assert_cmpuint (layout->partition_sig_len, ==, + VALIDITY_PARTITION_SIG_SIZE); + + /* Verify partitions are ordered and non-overlapping */ + for (guint p = 0; p < layout->num_partitions; p++) + { + const ValidityPartition *part = &layout->partitions[p]; + g_assert_cmpuint (part->size, >, 0); + + if (p > 0) + { + const ValidityPartition *prev = &layout->partitions[p - 1]; + g_assert_cmpuint (part->offset, >=, + prev->offset + prev->size); + } + } + } +} + +/* ================================================================ + * T7.9: Blob sizes match expected values from python-validity + * ================================================================ */ +static void +test_hal_blob_sizes (void) +{ + const ValidityDeviceDesc *desc_9a = + validity_hal_device_lookup (VALIDITY_DEV_9A); + + g_assert_nonnull (desc_9a); + + /* 009a blobs: init=581, clean_slate=741, reset=12037, dbe=3621 */ + g_assert_cmpuint (desc_9a->init_hardcoded_len, ==, 581); + g_assert_cmpuint (desc_9a->init_clean_slate_len, ==, 741); + g_assert_cmpuint (desc_9a->reset_blob_len, ==, 12037); + g_assert_cmpuint (desc_9a->db_write_enable_len, ==, 3621); + + const ValidityDeviceDesc *desc_90 = + validity_hal_device_lookup (VALIDITY_DEV_90); + g_assert_nonnull (desc_90); + + /* 0090 blobs: init=485, no clean_slate, reset=11493, dbe=1765 */ + g_assert_cmpuint (desc_90->init_hardcoded_len, ==, 485); + g_assert_cmpuint (desc_90->reset_blob_len, ==, 11493); + g_assert_cmpuint (desc_90->db_write_enable_len, ==, 1765); +} + +/* ================================================================ + * T7.10: Lookup consistency — by-type and by-PID return same pointer + * ================================================================ */ +static void +test_hal_lookup_consistency (void) +{ + const ValidityDeviceDesc *by_type = + validity_hal_device_lookup (VALIDITY_DEV_9A); + const ValidityDeviceDesc *by_pid = + validity_hal_device_lookup_by_pid (0x06cb, 0x009a); + + g_assert_true (by_type == by_pid); +} + +/* ================================================================ + * Tests: SENSOR + * ================================================================ */ + + +/* ================================================================ + * T4.1: test_identify_sensor_parse + * + * Verify that a valid cmd 0x75 response is parsed correctly into + * a ValiditySensorIdent (hw_major + hw_version). + * + * Wire format (after 2-byte status stripped): + * [zeroes:4 LE] [version:2 LE] [major:2 LE] + * ================================================================ */ +static void +test_identify_sensor_parse (void) +{ + ValiditySensorIdent ident; + + /* Build synthetic response: zeroes=0, version=0x13, major=0x004a */ + guint8 data[8]; + + FP_WRITE_UINT32_LE (&data[0], 0); /* zeroes */ + FP_WRITE_UINT16_LE (&data[4], 0x0013); /* version */ + FP_WRITE_UINT16_LE (&data[6], 0x004a); /* major */ + + gboolean ok = validity_sensor_parse_identify (data, sizeof (data), &ident); + + g_assert_true (ok); + g_assert_cmpuint (ident.hw_major, ==, 0x004a); + g_assert_cmpuint (ident.hw_version, ==, 0x0013); +} + +/* ================================================================ + * T4.2: test_identify_sensor_parse_truncated + * + * Verify that a response shorter than 8 bytes returns FALSE. + * ================================================================ */ +static void +test_identify_sensor_parse_truncated (void) +{ + ValiditySensorIdent ident; + + guint8 data[7] = { 0 }; + + g_assert_false (validity_sensor_parse_identify (data, sizeof (data), &ident)); + /* Also test with 0 length */ + g_assert_false (validity_sensor_parse_identify (data, 0, &ident)); +} + +/* ================================================================ + * T4.3: test_device_info_lookup_exact + * + * Verify that lookup with major=0x004a, version=0x13 returns the + * correct DeviceInfo for the ThinkPad T480s sensor. + * ================================================================ */ +static void +test_device_info_lookup_exact (void) +{ + const ValidityDeviceInfo *info; + + info = validity_device_info_lookup (0x004a, 0x13); + + g_assert_nonnull (info); + g_assert_cmpuint (info->major, ==, 0x004a); + g_assert_cmpuint (info->type, ==, 0x00b5); + g_assert_cmpuint (info->version, ==, 0x13); + g_assert_cmpstr (info->name, ==, "SYN 57K0 FM3297-02"); +} + +/* ================================================================ + * T4.4: test_device_info_lookup_another + * + * Verify that lookup with major=0x0071, version=0x01 returns + * the VSI 55E entry (type 0xdb). + * ================================================================ */ +static void +test_device_info_lookup_another (void) +{ + const ValidityDeviceInfo *info; + + info = validity_device_info_lookup (0x0071, 0x01); + + g_assert_nonnull (info); + g_assert_cmpuint (info->type, ==, 0x00db); + g_assert_cmpstr (info->name, ==, "VSI 55E FM72-001"); +} + +/* ================================================================ + * T4.5: test_device_info_lookup_unknown + * + * Verify that a completely unknown major returns NULL. + * ================================================================ */ +static void +test_device_info_lookup_unknown (void) +{ + const ValidityDeviceInfo *info; + + info = validity_device_info_lookup (0xffff, 0x01); + + g_assert_null (info); +} + +/* ================================================================ + * T4.6: test_device_info_lookup_fuzzy + * + * Verify that when version_mask == 0x00, the entry matches any + * version (fuzzy match). + * ================================================================ */ +static void +test_device_info_lookup_fuzzy (void) +{ + const ValidityDeviceInfo *info; + + /* major=0x0000 entries have version_mask=0x00 → always fuzzy match. + * But major=0x0000 needs to match the lookup major. */ + info = validity_device_info_lookup (0x0000, 0x42); + + /* Should match one of the wildcard entries */ + g_assert_nonnull (info); + g_assert_cmpuint (info->major, ==, 0x0000); +} + +/* ================================================================ + * T4.7: test_sensor_type_info_lookup + * + * Verify lookup of sensor type 0x00b5 returns correct geometry. + * ================================================================ */ +static void +test_sensor_type_info_lookup (void) +{ + const ValiditySensorTypeInfo *info; + + info = validity_sensor_type_info_lookup (0x00b5); + + g_assert_nonnull (info); + g_assert_cmpuint (info->sensor_type, ==, 0x00b5); + g_assert_cmpuint (info->bytes_per_line, ==, 0x78); + g_assert_cmpuint (info->repeat_multiplier, ==, 2); + g_assert_cmpuint (info->lines_per_calibration_data, ==, 112); + g_assert_cmpuint (info->line_width, ==, 112); + g_assert_nonnull (info->calibration_blob); + g_assert_cmpuint (info->calibration_blob_len, ==, 112); +} + +/* ================================================================ + * T4.8: test_sensor_type_info_lookup_db + * + * Verify lookup of sensor type 0x00db (55E) returns correct geometry. + * ================================================================ */ +static void +test_sensor_type_info_lookup_db (void) +{ + const ValiditySensorTypeInfo *info; + + info = validity_sensor_type_info_lookup (0x00db); + + g_assert_nonnull (info); + g_assert_cmpuint (info->bytes_per_line, ==, 0x98); + g_assert_cmpuint (info->repeat_multiplier, ==, 1); + g_assert_cmpuint (info->lines_per_calibration_data, ==, 144); + g_assert_cmpuint (info->line_width, ==, 144); +} + +/* ================================================================ + * T4.9: test_sensor_type_info_lookup_unknown + * + * Verify that an unknown sensor type returns NULL. + * ================================================================ */ +static void +test_sensor_type_info_lookup_unknown (void) +{ + g_assert_null (validity_sensor_type_info_lookup (0xbeef)); +} + +/* ================================================================ + * T4.10: test_factory_bits_cmd_format + * + * Verify that the factory bits command is built correctly. + * Expected: [0x6f] [0x00 0x0e] [0x00 0x00] [0x00 0x00 0x00 0x00] + * ================================================================ */ +static void +test_factory_bits_cmd_format (void) +{ + guint8 buf[16]; + gsize len; + + len = validity_sensor_build_factory_bits_cmd (0x0e00, buf, sizeof (buf)); + + g_assert_cmpuint (len, ==, 9); + g_assert_cmpuint (buf[0], ==, 0x6f); + /* tag = 0x0e00 LE */ + g_assert_cmpuint (buf[1], ==, 0x00); + g_assert_cmpuint (buf[2], ==, 0x0e); + /* pad 2 bytes */ + g_assert_cmpuint (buf[3], ==, 0x00); + g_assert_cmpuint (buf[4], ==, 0x00); + /* pad 4 bytes */ + g_assert_cmpuint (buf[5], ==, 0x00); + g_assert_cmpuint (buf[6], ==, 0x00); + g_assert_cmpuint (buf[7], ==, 0x00); + g_assert_cmpuint (buf[8], ==, 0x00); +} + +/* ================================================================ + * T4.11: test_factory_bits_cmd_buffer_too_small + * + * Verify that a too-small buffer returns 0. + * ================================================================ */ +static void +test_factory_bits_cmd_buffer_too_small (void) +{ + guint8 buf[4]; + gsize len; + + len = validity_sensor_build_factory_bits_cmd (0x0e00, buf, sizeof (buf)); + + g_assert_cmpuint (len, ==, 0); +} + +/* ================================================================ + * T4.12: test_identify_then_lookup + * + * End-to-end: parse identify_sensor response → DeviceInfo lookup → + * SensorTypeInfo lookup. Simulates the T480s sensor (06cb:009a). + * ================================================================ */ +static void +test_identify_then_lookup (void) +{ + ValiditySensorIdent ident; + const ValidityDeviceInfo *dev_info; + const ValiditySensorTypeInfo *type_info; + + /* Simulate cmd 0x75 response for T480s: major=0x004a, version=0x13 */ + guint8 data[8]; + + FP_WRITE_UINT32_LE (&data[0], 0); + FP_WRITE_UINT16_LE (&data[4], 0x0013); + FP_WRITE_UINT16_LE (&data[6], 0x004a); + + g_assert_true (validity_sensor_parse_identify (data, sizeof (data), &ident)); + g_assert_cmpuint (ident.hw_major, ==, 0x004a); + g_assert_cmpuint (ident.hw_version, ==, 0x0013); + + dev_info = validity_device_info_lookup (ident.hw_major, ident.hw_version); + g_assert_nonnull (dev_info); + g_assert_cmpuint (dev_info->type, ==, 0x00b5); + + type_info = validity_sensor_type_info_lookup (dev_info->type); + g_assert_nonnull (type_info); + g_assert_cmpuint (type_info->bytes_per_line, ==, 0x78); + g_assert_cmpuint (type_info->line_width, ==, 112); +} + +/* ================================================================ + * T4.13: test_sensor_state_lifecycle + * + * Verify that init zeros the state and clear frees allocated data. + * ================================================================ */ +static void +test_sensor_state_lifecycle (void) +{ + ValiditySensorState state; + + validity_sensor_state_init (&state); + g_assert_null (state.device_info); + g_assert_null (state.type_info); + g_assert_null (state.factory_bits); + g_assert_cmpuint (state.factory_bits_len, ==, 0); + + /* Simulate storing factory bits */ + state.factory_bits = g_memdup2 ("\x01\x02\x03", 3); + state.factory_bits_len = 3; + + validity_sensor_state_clear (&state); + g_assert_null (state.factory_bits); + g_assert_cmpuint (state.factory_bits_len, ==, 0); +} + +/* ================================================================ + * T4.14: test_calibration_blob_present + * + * Verify that the calibration blob for type 0x00b5 has expected + * first and last bytes (from python-validity generated_tables). + * ================================================================ */ +static void +test_calibration_blob_present (void) +{ + const ValiditySensorTypeInfo *info; + + info = validity_sensor_type_info_lookup (0x00b5); + g_assert_nonnull (info); + g_assert_nonnull (info->calibration_blob); + g_assert_cmpuint (info->calibration_blob_len, ==, 112); + + /* First byte: 0x9b, last byte: 0x06 */ + g_assert_cmpuint (info->calibration_blob[0], ==, 0x9b); + g_assert_cmpuint (info->calibration_blob[111], ==, 0x06); +} + +/* ================================================================ + * Tests: ENROLL + * ================================================================ */ + + +/* ================================================================ + * Helper: build a tagged block + * [tag:2LE][len:2LE][padding:MAGIC_LEN][payload:len] + * Total block size = 4 + MAGIC_LEN + len = MAGIC_LEN + len + 4 + * Wait — re-read the parser: + * tag(2LE) | len(2LE) => block_size = MAGIC_LEN + len + * so the full block is [tag:2][len:2] + body[MAGIC_LEN + len] + * No — looking at the code: pos + 4 reads tag+len, then + * block_size = MAGIC_LEN + len, and the block starts at data[pos]. + * Template: data[pos .. pos + block_size]. + * Header: data[pos + MAGIC_LEN .. pos + MAGIC_LEN + len]. + * Advance: pos += block_size. + * + * Actually re-reading more carefully: + * tag = data[pos], len = data[pos+2] + * block_size = MAGIC_LEN + len + * template = data[pos .. pos + block_size] + * So the 4 bytes of tag+len are INSIDE the block_size. + * MAGIC_LEN = 0x38 = 56 which is > 4, so tag+len fit inside. + * + * To build test data: write tag(2LE) at offset 0, len(2LE) at + * offset 2, then (MAGIC_LEN - 4) padding bytes, then len payload bytes. + * Total = MAGIC_LEN + len. + * ================================================================ */ +static guint8 * +build_block (guint16 tag, const guint8 *payload, guint16 payload_len, + gsize *out_len) +{ + gsize block_size = ENROLLMENT_MAGIC_LEN + payload_len; + guint8 *buf = g_malloc0 (block_size); + + FP_WRITE_UINT16_LE (buf, tag); + FP_WRITE_UINT16_LE (buf + 2, payload_len); + + if (payload && payload_len > 0) + memcpy (buf + ENROLLMENT_MAGIC_LEN, payload, payload_len); + + *out_len = block_size; + return buf; +} + +/* Wrap raw block data with the 2-byte declared_len prefix the parser expects: + * [declared_len:2LE][blocks...] + * declared_len = blocks_len (total size of all concatenated blocks). */ +static guint8 * +wrap_response (const guint8 *blocks, gsize blocks_len, gsize *out_len) +{ + *out_len = 2 + blocks_len; + + guint8 *buf = g_malloc (*out_len); + + FP_WRITE_UINT16_LE (buf, (guint16) blocks_len); + if (blocks && blocks_len > 0) + memcpy (buf + 2, blocks, blocks_len); + return buf; +} + +/* ================================================================ + * T8.1: parse empty data — returns TRUE, all fields NULL + * ================================================================ */ +static void +test_parse_empty (void) +{ + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (NULL, 0, &result); + + /* Empty data (len < 2) → parser returns FALSE */ + g_assert_false (ok); +} + +/* ================================================================ + * T8.2: parse single template block (tag=0) + * ================================================================ */ +static void +test_parse_template_block (void) +{ + guint8 payload[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + gsize block_len; + g_autofree guint8 *block = build_block (0, payload, sizeof (payload), + &block_len); + gsize resp_len; + g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, resp_len, &result); + + g_assert_true (ok); + g_assert_nonnull (result.template_data); + g_assert_cmpuint (result.template_len, ==, block_len); + g_assert_null (result.header); + g_assert_null (result.tid); + + enrollment_update_result_clear (&result); +} + +/* ================================================================ + * T8.3: parse header block (tag=1) + * ================================================================ */ +static void +test_parse_header_block (void) +{ + guint8 payload[] = { 0x01, 0x02, 0x03 }; + gsize block_len; + g_autofree guint8 *block = build_block (1, payload, sizeof (payload), + &block_len); + gsize resp_len; + g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, resp_len, &result); + + g_assert_true (ok); + g_assert_nonnull (result.header); + g_assert_cmpuint (result.header_len, ==, sizeof (payload)); + g_assert_cmpmem (result.header, result.header_len, payload, sizeof (payload)); + g_assert_null (result.template_data); + g_assert_null (result.tid); + + enrollment_update_result_clear (&result); +} + +/* ================================================================ + * T8.4: parse tid block (tag=3) — signals enrollment complete + * ================================================================ */ +static void +test_parse_tid_block (void) +{ + guint8 payload[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; + gsize block_len; + g_autofree guint8 *block = build_block (3, payload, sizeof (payload), + &block_len); + gsize resp_len; + g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, resp_len, &result); + + g_assert_true (ok); + g_assert_nonnull (result.tid); + g_assert_cmpuint (result.tid_len, ==, sizeof (payload)); + g_assert_cmpmem (result.tid, result.tid_len, payload, sizeof (payload)); + g_assert_null (result.template_data); + g_assert_null (result.header); + + enrollment_update_result_clear (&result); +} + +/* ================================================================ + * T8.5: parse multiple blocks — template + header + tid + * ================================================================ */ +static void +test_parse_multiple_blocks (void) +{ + guint8 tmpl_payload[] = { 0x11, 0x22 }; + guint8 hdr_payload[] = { 0x33, 0x44, 0x55 }; + guint8 tid_payload[] = { 0x66 }; + + gsize tmpl_len, hdr_len, tid_len; + g_autofree guint8 *tmpl = build_block (0, tmpl_payload, + sizeof (tmpl_payload), &tmpl_len); + g_autofree guint8 *hdr = build_block (1, hdr_payload, + sizeof (hdr_payload), &hdr_len); + g_autofree guint8 *tid = build_block (3, tid_payload, + sizeof (tid_payload), &tid_len); + + /* Concatenate all three blocks, then wrap with length prefix */ + gsize blocks_total = tmpl_len + hdr_len + tid_len; + g_autofree guint8 *blocks = g_malloc (blocks_total); + + memcpy (blocks, tmpl, tmpl_len); + memcpy (blocks + tmpl_len, hdr, hdr_len); + memcpy (blocks + tmpl_len + hdr_len, tid, tid_len); + + gsize resp_len; + g_autofree guint8 *data = wrap_response (blocks, blocks_total, &resp_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, resp_len, &result); + + g_assert_true (ok); + g_assert_nonnull (result.template_data); + g_assert_nonnull (result.header); + g_assert_nonnull (result.tid); + g_assert_cmpuint (result.template_len, ==, tmpl_len); + g_assert_cmpuint (result.header_len, ==, sizeof (hdr_payload)); + g_assert_cmpuint (result.tid_len, ==, sizeof (tid_payload)); + + enrollment_update_result_clear (&result); +} + +/* ================================================================ + * T8.6: parse truncated data — stops before reading past buffer + * ================================================================ */ +static void +test_parse_truncated (void) +{ + /* Build a response where the declared length is consistent with data_len + * but the block content is too short for a full block to be parsed. + * declared_len = 6, so data = [06 00][tag:2][len:2][2 more bytes] + * The block_size = MAGIC_LEN + len will exceed 8 for any len > 0, + * so the parser's "pos + block_size > data_len" check will skip it. */ + guint8 data[8]; + + FP_WRITE_UINT16_LE (data, 6); /* declared_len = 6 */ + FP_WRITE_UINT16_LE (data + 2, 0); /* tag = 0 (template) */ + FP_WRITE_UINT16_LE (data + 4, 10); /* len = 10 → block_size = MAGIC_LEN + 10 > 8 */ + data[6] = 0; + data[7] = 0; + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, sizeof (data), &result); + + g_assert_true (ok); + /* No fields should be populated since the block was truncated */ + g_assert_null (result.template_data); + g_assert_null (result.header); + g_assert_null (result.tid); +} + +/* ================================================================ + * T8.7: parse unknown tag — silently skipped + * ================================================================ */ +static void +test_parse_unknown_tag (void) +{ + guint8 payload[] = { 0x99 }; + gsize block_len; + g_autofree guint8 *block = build_block (42, payload, sizeof (payload), + &block_len); + gsize resp_len; + g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, resp_len, &result); + + g_assert_true (ok); + g_assert_null (result.template_data); + g_assert_null (result.header); + g_assert_null (result.tid); +} + +/* ================================================================ + * T8.8: result_clear — frees and zeroes + * ================================================================ */ +static void +test_result_clear (void) +{ + EnrollmentUpdateResult result; + + result.header = g_malloc (10); + result.header_len = 10; + result.template_data = g_malloc (20); + result.template_len = 20; + result.tid = g_malloc (5); + result.tid_len = 5; + + enrollment_update_result_clear (&result); + + g_assert_null (result.header); + g_assert_null (result.template_data); + g_assert_null (result.tid); + g_assert_cmpuint (result.header_len, ==, 0); + g_assert_cmpuint (result.template_len, ==, 0); + g_assert_cmpuint (result.tid_len, ==, 0); +} + +/* ================================================================ + * T8.9: parse zero-length payload — tag present but no data + * ================================================================ */ +static void +test_parse_zero_length_payload (void) +{ + gsize block_len; + g_autofree guint8 *block = build_block (1, NULL, 0, &block_len); + gsize resp_len; + g_autofree guint8 *data = wrap_response (block, block_len, &resp_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, resp_len, &result); + + g_assert_true (ok); + /* Tag 1 with len=0: header should be NULL (len > 0 check in parser) */ + g_assert_null (result.header); +} + +/* ================================================================ + * Tests: FWEXT + * ================================================================ */ + + +/* ================================================================ + * T3.1: test_fw_info_parse_present + * + * Verify that a valid GET_FW_INFO response (status=OK) with 1 module + * is parsed correctly into ValidityFwInfo. + * ================================================================ */ +static void +test_fw_info_parse_present (void) +{ + ValidityFwInfo info; + + /* Build a synthetic GET_FW_INFO response: + * major(2) + minor(2) + modcnt(2) + buildtime(4) = 10 header bytes + * + 1 module * 12 bytes = 12 + * Total = 22 bytes (data after 2-byte status, which is stripped) */ + guint8 data[22]; + + memset (data, 0, sizeof (data)); + + /* major = 6 */ + data[0] = 6; + data[1] = 0; + /* minor = 7 */ + data[2] = 7; + data[3] = 0; + /* module_count = 1 */ + data[4] = 1; + data[5] = 0; + /* buildtime = 0x12345678 LE */ + data[6] = 0x78; + data[7] = 0x56; + data[8] = 0x34; + data[9] = 0x12; + /* Module 0: type=3, subtype=4, major=1, minor=2, size=0x1000 */ + data[10] = 3; + data[11] = 0; + data[12] = 4; + data[13] = 0; + data[14] = 1; + data[15] = 0; + data[16] = 2; + data[17] = 0; + data[18] = 0x00; + data[19] = 0x10; + data[20] = 0x00; + data[21] = 0x00; + + gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data), + VCSFW_STATUS_OK, &info); + + g_assert_true (ok); + g_assert_true (info.loaded); + g_assert_cmpuint (info.major, ==, 6); + g_assert_cmpuint (info.minor, ==, 7); + g_assert_cmpuint (info.module_count, ==, 1); + g_assert_cmpuint (info.buildtime, ==, 0x12345678); + g_assert_cmpuint (info.modules[0].type, ==, 3); + g_assert_cmpuint (info.modules[0].subtype, ==, 4); + g_assert_cmpuint (info.modules[0].major, ==, 1); + g_assert_cmpuint (info.modules[0].minor, ==, 2); + g_assert_cmpuint (info.modules[0].size, ==, 0x1000); +} + +/* ================================================================ + * T3.2: test_fw_info_parse_absent + * + * Verify that status VCSFW_STATUS_NO_FW sets loaded=FALSE. + * ================================================================ */ +static void +test_fw_info_parse_absent (void) +{ + ValidityFwInfo info; + + gboolean ok = validity_fwext_parse_fw_info (NULL, 0, + VCSFW_STATUS_NO_FW, &info); + + g_assert_true (ok); + g_assert_false (info.loaded); +} + +/* ================================================================ + * T3.2b: test_fw_info_parse_unknown_status + * + * Verify that an unexpected status returns FALSE. + * ================================================================ */ +static void +test_fw_info_parse_unknown_status (void) +{ + ValidityFwInfo info; + + g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING, + "*unexpected status*"); + + gboolean ok = validity_fwext_parse_fw_info (NULL, 0, 0x9999, &info); + + g_test_assert_expected_messages (); + + g_assert_false (ok); + g_assert_false (info.loaded); +} + +/* ================================================================ + * T3.2c: test_fw_info_parse_truncated + * + * Verify that status=OK but data too short returns FALSE. + * ================================================================ */ +static void +test_fw_info_parse_truncated (void) +{ + ValidityFwInfo info; + guint8 data[5] = { 0 }; + + g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING, + "*too short*"); + + gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data), + VCSFW_STATUS_OK, &info); + + g_test_assert_expected_messages (); + + g_assert_false (ok); +} + +/* ================================================================ + * T3.3: test_xpfwext_file_parse + * + * Create a synthetic .xpfwext file in a temp dir and verify parsing. + * Format: header + 0x1A + payload + 256-byte signature + * ================================================================ */ +static void +test_xpfwext_file_parse (void) +{ + g_autoptr(GError) error = NULL; + ValidityFwextFile fwext; + gchar *tmpdir; + g_autofree gchar *path = NULL; + + tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); + g_assert_no_error (error); + + path = g_build_filename (tmpdir, "test.xpfwext", NULL); + + /* Build file: "HDR" + 0x1A + 8 bytes payload + 256 bytes sig */ + gsize header_len = 4; /* "HDR" + 0x1A */ + gsize payload_len = 8; + gsize sig_len = 256; + gsize total = header_len + payload_len + sig_len; + + guint8 *content = g_malloc0 (total); + + content[0] = 'H'; + content[1] = 'D'; + content[2] = 'R'; + content[3] = 0x1A; + + /* Payload: 0x01..0x08 */ + for (gsize i = 0; i < payload_len; i++) + content[header_len + i] = (guint8) (i + 1); + + /* Signature: 0xAA repeated */ + memset (content + header_len + payload_len, 0xAA, sig_len); + + g_file_set_contents (path, (gchar *) content, total, &error); + g_assert_no_error (error); + + gboolean ok = validity_fwext_load_file (path, &fwext, &error); + g_assert_no_error (error); + g_assert_true (ok); + + g_assert_cmpuint (fwext.payload_len, ==, payload_len); + + /* Check payload content */ + for (gsize i = 0; i < payload_len; i++) + g_assert_cmpuint (fwext.payload[i], ==, i + 1); + + /* Check signature */ + for (gsize i = 0; i < sig_len; i++) + g_assert_cmpuint (fwext.signature[i], ==, 0xAA); + + validity_fwext_file_clear (&fwext); + g_assert_null (fwext.payload); + g_assert_cmpuint (fwext.payload_len, ==, 0); + + /* Cleanup */ + g_unlink (path); + g_rmdir (tmpdir); + g_free (content); + g_free (tmpdir); +} + +/* ================================================================ + * T3.3b: test_xpfwext_file_no_delimiter + * + * Verify that a file without 0x1A delimiter fails to parse. + * ================================================================ */ +static void +test_xpfwext_file_no_delimiter (void) +{ + g_autoptr(GError) error = NULL; + ValidityFwextFile fwext; + gchar *tmpdir; + g_autofree gchar *path = NULL; + + tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); + g_assert_no_error (error); + + path = g_build_filename (tmpdir, "bad.xpfwext", NULL); + + /* All 0xFF — no 0x1A delimiter */ + guint8 content[300]; + + memset (content, 0xFF, sizeof (content)); + + g_file_set_contents (path, (gchar *) content, sizeof (content), &error); + g_assert_no_error (error); + + gboolean ok = validity_fwext_load_file (path, &fwext, &error); + g_assert_false (ok); + g_assert_nonnull (error); + + g_unlink (path); + g_rmdir (tmpdir); + g_free (tmpdir); +} + +/* ================================================================ + * T3.3c: test_xpfwext_file_too_short + * + * Verify that a file with valid header but data shorter than + * signature size fails. + * ================================================================ */ +static void +test_xpfwext_file_too_short (void) +{ + g_autoptr(GError) error = NULL; + ValidityFwextFile fwext; + gchar *tmpdir; + g_autofree gchar *path = NULL; + + tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); + g_assert_no_error (error); + + path = g_build_filename (tmpdir, "short.xpfwext", NULL); + + /* "X" + 0x1A + 10 bytes (< 257 needed for sig + 1 byte payload) */ + guint8 content[12]; + + content[0] = 'X'; + content[1] = 0x1A; + memset (content + 2, 0, 10); + + g_file_set_contents (path, (gchar *) content, sizeof (content), &error); + g_assert_no_error (error); + + gboolean ok = validity_fwext_load_file (path, &fwext, &error); + g_assert_false (ok); + g_assert_nonnull (error); + + g_unlink (path); + g_rmdir (tmpdir); + g_free (tmpdir); +} + +/* ================================================================ + * T3.4: test_flash_write_cmd_format + * + * Verify that build_write_flash produces the correct wire format: + * [0x41, partition, 1, 0, 0, offset_LE32, len_LE32, data...] + * ================================================================ */ +static void +test_flash_write_cmd_format (void) +{ + guint8 payload[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + guint8 cmd[13 + sizeof (payload)]; + gsize cmd_len; + + validity_fwext_build_write_flash (2, 0x1000, payload, sizeof (payload), + cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 13 + sizeof (payload)); + g_assert_cmpuint (cmd[0], ==, 0x41); /* WRITE_FLASH command */ + g_assert_cmpuint (cmd[1], ==, 2); /* partition */ + g_assert_cmpuint (cmd[2], ==, 1); /* flag */ + g_assert_cmpuint (cmd[3], ==, 0); /* reserved low */ + g_assert_cmpuint (cmd[4], ==, 0); /* reserved high */ + + /* offset = 0x1000 LE */ + g_assert_cmpuint (cmd[5], ==, 0x00); + g_assert_cmpuint (cmd[6], ==, 0x10); + g_assert_cmpuint (cmd[7], ==, 0x00); + g_assert_cmpuint (cmd[8], ==, 0x00); + + /* length = 4 LE */ + g_assert_cmpuint (cmd[9], ==, 0x04); + g_assert_cmpuint (cmd[10], ==, 0x00); + g_assert_cmpuint (cmd[11], ==, 0x00); + g_assert_cmpuint (cmd[12], ==, 0x00); + + /* data */ + g_assert_cmpmem (cmd + 13, sizeof (payload), payload, sizeof (payload)); +} + +/* ================================================================ + * T3.5: test_fw_sig_cmd_format + * + * Verify that build_write_fw_sig produces the correct wire format: + * [0x42, partition, 0, len_LE16, signature...] + * ================================================================ */ +static void +test_fw_sig_cmd_format (void) +{ + guint8 sig[256]; + guint8 cmd[5 + 256]; + gsize cmd_len; + + memset (sig, 0xBB, sizeof (sig)); + + validity_fwext_build_write_fw_sig (2, sig, sizeof (sig), cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 5 + 256); + g_assert_cmpuint (cmd[0], ==, 0x42); /* WRITE_FW_SIG command */ + g_assert_cmpuint (cmd[1], ==, 2); /* partition */ + g_assert_cmpuint (cmd[2], ==, 0); /* reserved */ + + /* sig length = 256 LE */ + g_assert_cmpuint (cmd[3], ==, 0x00); + g_assert_cmpuint (cmd[4], ==, 0x01); + + g_assert_cmpmem (cmd + 5, 256, sig, 256); +} + +/* ================================================================ + * T3.6: test_chunk_iteration + * + * Verify that building write_flash with increasing offsets covers + * the entire payload. Simulate the chunk loop used by the SSM. + * ================================================================ */ +static void +test_chunk_iteration (void) +{ + gsize payload_len = 0x2800; /* 10 KB = 2.5 chunks of 4 KB */ + gsize write_offset = 0; + guint chunk_count = 0; + const gsize CHUNK_SIZE = 0x1000; + + while (write_offset < payload_len) + { + gsize remaining = payload_len - write_offset; + gsize chunk_size = MIN (remaining, CHUNK_SIZE); + + g_assert_cmpuint (chunk_size, >, 0); + g_assert_cmpuint (chunk_size, <=, CHUNK_SIZE); + + write_offset += chunk_size; + chunk_count++; + } + + g_assert_cmpuint (write_offset, ==, payload_len); + g_assert_cmpuint (chunk_count, ==, 3); /* 4096 + 4096 + 2048 */ +} + +/* ================================================================ + * T3.7: test_hw_reg_cmd_format + * + * Verify WRITE_HW_REG32 and READ_HW_REG32 command formats. + * ================================================================ */ +static void +test_hw_reg_write_cmd_format (void) +{ + guint8 cmd[10]; + gsize cmd_len; + + validity_fwext_build_write_hw_reg32 (0x8000205C, 7, cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 10); + g_assert_cmpuint (cmd[0], ==, 0x08); /* WRITE_HW_REG32 command */ + + /* address = 0x8000205C LE */ + g_assert_cmpuint (cmd[1], ==, 0x5C); + g_assert_cmpuint (cmd[2], ==, 0x20); + g_assert_cmpuint (cmd[3], ==, 0x00); + g_assert_cmpuint (cmd[4], ==, 0x80); + + /* value = 7 LE */ + g_assert_cmpuint (cmd[5], ==, 0x07); + g_assert_cmpuint (cmd[6], ==, 0x00); + g_assert_cmpuint (cmd[7], ==, 0x00); + g_assert_cmpuint (cmd[8], ==, 0x00); + + /* size = 4 */ + g_assert_cmpuint (cmd[9], ==, 4); +} + +static void +test_hw_reg_read_cmd_format (void) +{ + guint8 cmd[6]; + gsize cmd_len; + + validity_fwext_build_read_hw_reg32 (0x80002080, cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 6); + g_assert_cmpuint (cmd[0], ==, 0x07); /* READ_HW_REG32 command */ + + /* address = 0x80002080 LE */ + g_assert_cmpuint (cmd[1], ==, 0x80); + g_assert_cmpuint (cmd[2], ==, 0x20); + g_assert_cmpuint (cmd[3], ==, 0x00); + g_assert_cmpuint (cmd[4], ==, 0x80); + + /* size = 4 */ + g_assert_cmpuint (cmd[5], ==, 4); +} + +static void +test_hw_reg_read_parse (void) +{ + guint32 value; + guint8 data[] = { 0x02, 0x00, 0x00, 0x00 }; + + gboolean ok = validity_fwext_parse_read_hw_reg32 (data, sizeof (data), + &value); + + g_assert_true (ok); + g_assert_cmpuint (value, ==, 2); + + /* Too short should fail */ + ok = validity_fwext_parse_read_hw_reg32 (data, 3, &value); + g_assert_false (ok); +} + +/* ================================================================ + * T3.8: test_firmware_filename + * + * Verify firmware filename mapping for known PIDs. + * ================================================================ */ +static void +test_firmware_filename (void) +{ + const gchar *name; + + name = validity_fwext_get_firmware_name (0x06cb, 0x009a); + g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); + + name = validity_fwext_get_firmware_name (0x138a, 0x0090); + g_assert_cmpstr (name, ==, "6_07f_Lenovo.xpfwext"); + + name = validity_fwext_get_firmware_name (0x138a, 0x0097); + g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); + + name = validity_fwext_get_firmware_name (0x138a, 0x009d); + g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); + + /* Unknown PID should return NULL */ + name = validity_fwext_get_firmware_name (0x1234, 0x5678); + g_assert_null (name); +} + +/* ================================================================ + * T3.9: test_missing_firmware_file + * + * Verify that find_firmware returns an error when the file is not + * found in any search path. + * ================================================================ */ +static void +test_missing_firmware_file (void) +{ + g_autoptr(GError) error = NULL; + + /* This should fail since firmware files aren't installed in CI */ + g_autofree gchar *path = validity_fwext_find_firmware (0x06cb, 0x009a, + &error); + + /* It either found a file or returned an error — both are valid. + * In CI, it should be an error. On a real system, it might succeed. */ + if (path == NULL) + { + g_assert_nonnull (error); + g_assert_true (g_error_matches (error, FP_DEVICE_ERROR, + FP_DEVICE_ERROR_DATA_NOT_FOUND)); + } + else + { + g_assert_no_error (error); + g_assert_true (g_file_test (path, G_FILE_TEST_IS_REGULAR)); + } +} + +/* ================================================================ + * T3.9b: test_unsupported_pid_firmware + * + * Verify that find_firmware for an unknown PID returns NOT_SUPPORTED. + * ================================================================ */ +static void +test_unsupported_pid_firmware (void) +{ + g_autoptr(GError) error = NULL; + g_autofree gchar *path = validity_fwext_find_firmware (0x1234, 0x5678, + &error); + + g_assert_null (path); + g_assert_nonnull (error); + g_assert_true (g_error_matches (error, FP_DEVICE_ERROR, + FP_DEVICE_ERROR_NOT_SUPPORTED)); +} + +/* ================================================================ + * T3.10: test_fwext_db_write_enable_blob + * + * Verify that db_write_enable blob is returned for supported PID + * and NULL for unsupported PID. + * ================================================================ */ +static void +test_fwext_db_write_enable_blob (void) +{ + gsize len; + const guint8 *blob; + + blob = validity_fwext_get_db_write_enable (0x06cb, 0x009a, &len); + g_assert_nonnull (blob); + g_assert_cmpuint (len, ==, 3621); + + blob = validity_fwext_get_db_write_enable (0x1234, 0x5678, &len); + g_assert_null (blob); + g_assert_cmpuint (len, ==, 0); +} + +/* ================================================================ + * T3.11: test_reboot_cmd_format + * + * Verify reboot command: 0x05, 0x02, 0x00 + * ================================================================ */ +static void +test_reboot_cmd_format (void) +{ + guint8 cmd[3]; + gsize cmd_len; + + validity_fwext_build_reboot (cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 3); + g_assert_cmpuint (cmd[0], ==, 0x05); + g_assert_cmpuint (cmd[1], ==, 0x02); + g_assert_cmpuint (cmd[2], ==, 0x00); +} + +/* ================================================================ + * Regression: fwext_file_clear on already-cleared struct + * + * Double-free guard. + * ================================================================ */ +static void +test_file_clear_idempotent (void) +{ + ValidityFwextFile fwext = { 0 }; + + /* Clear an empty struct — should not crash */ + validity_fwext_file_clear (&fwext); + validity_fwext_file_clear (&fwext); + + g_assert_null (fwext.payload); + g_assert_cmpuint (fwext.payload_len, ==, 0); +} + +/* ================================================================ + * Tests: DB + * ================================================================ */ + + +/* ================================================================ + * T6.1: test_cmd_db_info + * + * Verify cmd 0x45 (DB info) is a single-byte command. + * ================================================================ */ +static void +test_cmd_db_info (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_info (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 1); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DB_INFO); +} + +/* ================================================================ + * T6.2: test_cmd_get_user_storage + * + * Verify cmd 0x4B format with a known storage name. + * ================================================================ */ +static void +test_cmd_get_user_storage (void) +{ + gsize len; + const gchar *name = "StgWindsor"; + gsize name_len = strlen (name) + 1; /* includes NUL */ + g_autofree guint8 *cmd = validity_db_build_cmd_get_user_storage (name, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 1 + 2 + 2 + name_len); /* cmd + dbid + name_len + name */ + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER_STORAGE); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0); /* dbid = 0 → lookup by name */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, name_len); + g_assert_cmpmem (&cmd[5], name_len, name, name_len); +} + +/* ================================================================ + * T6.3: test_cmd_get_user_storage_null_name + * + * When name is NULL, should produce a command with zero name_len. + * ================================================================ */ +static void +test_cmd_get_user_storage_null_name (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_get_user_storage (NULL, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 5); /* cmd(1) + dbid(2) + name_len(2) */ + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER_STORAGE); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); /* name_len = 0 */ +} + +/* ================================================================ + * T6.4: test_cmd_get_user + * + * Verify cmd 0x4A format for get-by-dbid. + * ================================================================ */ +static void +test_cmd_get_user (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_get_user (0x1234, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 7); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x1234); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); +} + +/* ================================================================ + * T6.5: test_cmd_lookup_user + * + * Verify cmd 0x4A format for lookup-by-identity. + * ================================================================ */ +static void +test_cmd_lookup_user (void) +{ + gsize len; + guint8 identity[] = { 0x01, 0x02, 0x03, 0x04 }; + g_autofree guint8 *cmd = validity_db_build_cmd_lookup_user ( + 0x0003, identity, sizeof (identity), &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 7 + sizeof (identity)); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0); /* dbid = 0 */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0x0003); /* storage */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, sizeof (identity)); + g_assert_cmpmem (&cmd[7], sizeof (identity), identity, sizeof (identity)); +} + +/* ================================================================ + * T6.6: test_cmd_new_record + * + * Verify cmd 0x47 format. + * ================================================================ */ +static void +test_cmd_new_record (void) +{ + gsize len; + guint8 data[] = { 0xAA, 0xBB }; + g_autofree guint8 *cmd = validity_db_build_cmd_new_record ( + 0x0003, 0x0005, 0x0003, data, sizeof (data), &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 9 + sizeof (data)); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_NEW_RECORD); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x0003); /* parent */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0x0005); /* type */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0x0003); /* storage */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, sizeof (data)); + g_assert_cmpmem (&cmd[9], sizeof (data), data, sizeof (data)); +} + +/* ================================================================ + * T6.7: test_cmd_del_record + * + * Verify cmd 0x48 format. + * ================================================================ */ +static void +test_cmd_del_record (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_del_record (0xABCD, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DEL_RECORD); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0xABCD); +} + +/* ================================================================ + * T6.8: test_cmd_create_enrollment + * + * Verify cmd 0x69 start and end variants. + * ================================================================ */ +static void +test_cmd_create_enrollment (void) +{ + gsize len; + + /* Start enrollment */ + g_autofree guint8 *cmd_start = validity_db_build_cmd_create_enrollment (TRUE, &len); + + g_assert_nonnull (cmd_start); + g_assert_cmpuint (len, ==, 5); + g_assert_cmpuint (cmd_start[0], ==, VCSFW_CMD_CREATE_ENROLLMENT); + g_assert_cmpuint (FP_READ_UINT32_LE (&cmd_start[1]), ==, 1); + + /* End enrollment */ + g_autofree guint8 *cmd_end = validity_db_build_cmd_create_enrollment (FALSE, &len); + g_assert_nonnull (cmd_end); + g_assert_cmpuint (len, ==, 5); + g_assert_cmpuint (cmd_end[0], ==, VCSFW_CMD_CREATE_ENROLLMENT); + g_assert_cmpuint (FP_READ_UINT32_LE (&cmd_end[1]), ==, 0); +} + +/* ================================================================ + * T6.9: test_cmd_enrollment_update_start + * + * Verify cmd 0x68 format. + * ================================================================ */ +static void +test_cmd_enrollment_update_start (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_enrollment_update_start (0x12345678, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 9); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_ENROLLMENT_UPDATE_START); + g_assert_cmpuint (FP_READ_UINT32_LE (&cmd[1]), ==, 0x12345678); + g_assert_cmpuint (FP_READ_UINT32_LE (&cmd[5]), ==, 0); +} + +/* ================================================================ + * T6.10: test_cmd_match_finger + * + * Verify cmd 0x5E format (13 bytes per python-validity). + * ================================================================ */ +static void +test_cmd_match_finger (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_match_finger (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 13); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_FINGER); + g_assert_cmpuint (cmd[1], ==, 0x02); + g_assert_cmpuint (cmd[2], ==, 0xFF); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, 1); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[9]), ==, 0); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[11]), ==, 0); +} + +/* ================================================================ + * T6.11: test_cmd_get_match_result + * + * Verify cmd 0x60 format. + * ================================================================ */ +static void +test_cmd_get_match_result (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_get_match_result (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 5); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_MATCH_RESULT); + /* remaining bytes should be 0 */ + g_assert_cmpuint (cmd[1], ==, 0); + g_assert_cmpuint (cmd[2], ==, 0); + g_assert_cmpuint (cmd[3], ==, 0); + g_assert_cmpuint (cmd[4], ==, 0); +} + +/* ================================================================ + * T6.12: test_cmd_match_cleanup + * + * Verify cmd 0x62 format. + * ================================================================ */ +static void +test_cmd_match_cleanup (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_match_cleanup (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 5); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_CLEANUP); +} + +/* ================================================================ + * T6.13: test_cmd_get_prg_status + * + * Verify cmd 0x51 both normal and extended variant. + * ================================================================ */ +static void +test_cmd_get_prg_status (void) +{ + gsize len; + + g_autofree guint8 *normal = validity_db_build_cmd_get_prg_status (FALSE, &len); + + g_assert_cmpuint (len, ==, 5); + g_assert_cmpuint (normal[0], ==, VCSFW_CMD_GET_PRG_STATUS); + /* Normal: 00000000 */ + g_assert_cmpuint (normal[1], ==, 0); + g_assert_cmpuint (normal[2], ==, 0); + g_assert_cmpuint (normal[3], ==, 0); + g_assert_cmpuint (normal[4], ==, 0); + + g_autofree guint8 *ext = validity_db_build_cmd_get_prg_status (TRUE, &len); + g_assert_cmpuint (len, ==, 5); + g_assert_cmpuint (ext[0], ==, VCSFW_CMD_GET_PRG_STATUS); + /* Extended: 00200000 LE */ + g_assert_cmpuint (ext[1], ==, 0x00); + g_assert_cmpuint (ext[2], ==, 0x20); + g_assert_cmpuint (ext[3], ==, 0x00); + g_assert_cmpuint (ext[4], ==, 0x00); +} + +/* ================================================================ + * T6.14: test_cmd_capture_stop + * + * Verify cmd 0x04 format. + * ================================================================ */ +static void +test_cmd_capture_stop (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_capture_stop (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 1); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_CAPTURE_STOP); +} + +/* ================================================================ + * T6.15: test_cmd_call_cleanups + * + * Verify cmd 0x1a format. + * ================================================================ */ +static void +test_cmd_call_cleanups (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_call_cleanups (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 1); + g_assert_cmpuint (cmd[0], ==, 0x1a); +} + +/* ================================================================ + * T6.16: test_parse_db_info + * + * Construct a known db_info binary response and verify parsing. + * ================================================================ */ +static void +test_parse_db_info (void) +{ + guint8 data[0x1C]; /* 24 bytes header + 4 bytes for 2 roots */ + ValidityDbInfo info; + + memset (data, 0, sizeof (data)); + + /* Header: unknown1=1, unknown0=0, total=65536, used=1024, free=64512, records=10, n_roots=2 */ + FP_WRITE_UINT32_LE (&data[0], 1); /* unknown1 */ + FP_WRITE_UINT32_LE (&data[4], 0); /* unknown0 */ + FP_WRITE_UINT32_LE (&data[8], 65536); /* total */ + FP_WRITE_UINT32_LE (&data[12], 1024); /* used */ + FP_WRITE_UINT32_LE (&data[16], 64512); /* free */ + FP_WRITE_UINT16_LE (&data[20], 10); /* records */ + FP_WRITE_UINT16_LE (&data[22], 2); /* n_roots */ + FP_WRITE_UINT16_LE (&data[24], 0x0001); /* root[0] */ + FP_WRITE_UINT16_LE (&data[26], 0x0003); /* root[1] */ + + g_assert_true (validity_db_parse_info (data, sizeof (data), &info)); + + g_assert_cmpuint (info.unknown1, ==, 1); + g_assert_cmpuint (info.unknown0, ==, 0); + g_assert_cmpuint (info.total, ==, 65536); + g_assert_cmpuint (info.used, ==, 1024); + g_assert_cmpuint (info.free_space, ==, 64512); + g_assert_cmpuint (info.records, ==, 10); + g_assert_cmpuint (info.n_roots, ==, 2); + g_assert_nonnull (info.roots); + g_assert_cmpuint (info.roots[0], ==, 1); + g_assert_cmpuint (info.roots[1], ==, 3); + + validity_db_info_clear (&info); +} + +/* ================================================================ + * T6.17: test_parse_db_info_too_short + * + * A response shorter than 24 bytes should fail. + * ================================================================ */ +static void +test_parse_db_info_too_short (void) +{ + guint8 data[20] = { 0 }; + ValidityDbInfo info; + + g_assert_false (validity_db_parse_info (data, sizeof (data), &info)); +} + +/* ================================================================ + * T6.18: test_parse_user_storage + * + * Construct a user storage response with 2 users and verify. + * ================================================================ */ +static void +test_parse_user_storage (void) +{ + /* Header: dbid=3, user_count=2, name_sz=11, unknown=0 + * User table: {dbid=10, val_sz=100}, {dbid=11, val_sz=200} + * Name: "StgWindsor\0" */ + gsize name_len = strlen ("StgWindsor") + 1; + gsize total = 8 + 2 * 4 + name_len; + g_autofree guint8 *data = g_new0 (guint8, total); + + FP_WRITE_UINT16_LE (&data[0], 3); /* dbid */ + FP_WRITE_UINT16_LE (&data[2], 2); /* user_count */ + FP_WRITE_UINT16_LE (&data[4], name_len); /* name_sz */ + FP_WRITE_UINT16_LE (&data[6], 0); /* unknown */ + + FP_WRITE_UINT16_LE (&data[8], 10); /* user[0].dbid */ + FP_WRITE_UINT16_LE (&data[10], 100); /* user[0].val_sz */ + FP_WRITE_UINT16_LE (&data[12], 11); /* user[1].dbid */ + FP_WRITE_UINT16_LE (&data[14], 200); /* user[1].val_sz */ + + memcpy (&data[16], "StgWindsor", name_len); + + ValidityUserStorage storage; + g_assert_true (validity_db_parse_user_storage (data, total, &storage)); + + g_assert_cmpuint (storage.dbid, ==, 3); + g_assert_cmpuint (storage.user_count, ==, 2); + g_assert_cmpstr (storage.name, ==, "StgWindsor"); + g_assert_nonnull (storage.user_dbids); + g_assert_cmpuint (storage.user_dbids[0], ==, 10); + g_assert_cmpuint (storage.user_dbids[1], ==, 11); + g_assert_cmpuint (storage.user_val_sizes[0], ==, 100); + g_assert_cmpuint (storage.user_val_sizes[1], ==, 200); + + validity_user_storage_clear (&storage); +} + +/* ================================================================ + * T6.19: test_parse_user + * + * Construct a user response with one finger and verify. + * ================================================================ */ +static void +test_parse_user (void) +{ + guint8 identity_bytes[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + /* Header: dbid=10, finger_count=1, unknown=0, identity_sz=4 + * Finger: dbid=20, subtype=2, storage=3, value_size=500 + * Identity: 4 bytes */ + gsize total = 8 + 8 + sizeof (identity_bytes); + g_autofree guint8 *data = g_new0 (guint8, total); + + FP_WRITE_UINT16_LE (&data[0], 10); /* dbid */ + FP_WRITE_UINT16_LE (&data[2], 1); /* finger_count */ + FP_WRITE_UINT16_LE (&data[4], 0); /* unknown */ + FP_WRITE_UINT16_LE (&data[6], sizeof (identity_bytes)); /* identity_sz */ + + /* Finger entry */ + FP_WRITE_UINT16_LE (&data[8], 20); /* finger.dbid */ + FP_WRITE_UINT16_LE (&data[10], 2); /* finger.subtype = right index */ + FP_WRITE_UINT16_LE (&data[12], 3); /* finger.storage */ + FP_WRITE_UINT16_LE (&data[14], 500); /* finger.value_size */ + + memcpy (&data[16], identity_bytes, sizeof (identity_bytes)); + + ValidityUser user; + g_assert_true (validity_db_parse_user (data, total, &user)); + + g_assert_cmpuint (user.dbid, ==, 10); + g_assert_cmpuint (user.finger_count, ==, 1); + g_assert_nonnull (user.fingers); + g_assert_cmpuint (user.fingers[0].dbid, ==, 20); + g_assert_cmpuint (user.fingers[0].subtype, ==, 2); + g_assert_cmpuint (user.fingers[0].storage, ==, 3); + g_assert_cmpuint (user.fingers[0].value_size, ==, 500); + g_assert_nonnull (user.identity); + g_assert_cmpuint (user.identity_len, ==, sizeof (identity_bytes)); + g_assert_cmpmem (user.identity, user.identity_len, + identity_bytes, sizeof (identity_bytes)); + + validity_user_clear (&user); +} + +/* ================================================================ + * T6.20: test_parse_new_record_id + * + * Verify parsing of new_record response (cmd 0x47). + * ================================================================ */ +static void +test_parse_new_record_id (void) +{ + guint16 record_id; + guint8 data[] = { 0x42, 0x00 }; + + g_assert_true (validity_db_parse_new_record_id (data, sizeof (data), &record_id)); + g_assert_cmpuint (record_id, ==, 0x0042); +} + +/* ================================================================ + * T6.21: test_parse_new_record_id_too_short + * + * A 1-byte response should fail. + * ================================================================ */ +static void +test_parse_new_record_id_too_short (void) +{ + guint16 record_id; + guint8 data[] = { 0x42 }; + + g_assert_false (validity_db_parse_new_record_id (data, sizeof (data), &record_id)); +} + +/* ================================================================ + * T6.22: test_build_identity + * + * Build a UUID identity and verify structure. + * ================================================================ */ +static void +test_build_identity (void) +{ + gsize len; + const gchar *uuid = "550e8400-e29b-41d4-a716-446655440000"; + g_autofree guint8 *id = validity_db_build_identity (uuid, &len); + + g_assert_nonnull (id); + /* Minimum size enforced */ + g_assert_cmpuint (len, >=, VALIDITY_IDENTITY_MIN_SIZE); + + /* type = SID (3) */ + g_assert_cmpuint (FP_READ_UINT32_LE (&id[0]), ==, VALIDITY_IDENTITY_TYPE_SID); + + /* len field = UUID string length */ + gsize uuid_len = strlen (uuid); + g_assert_cmpuint (FP_READ_UINT32_LE (&id[4]), ==, uuid_len); + + /* UUID payload */ + g_assert_cmpmem (&id[8], uuid_len, uuid, uuid_len); + + /* Remaining bytes should be zero-padded */ + for (gsize i = 8 + uuid_len; i < len; i++) + g_assert_cmpuint (id[i], ==, 0); +} + +/* ================================================================ + * T6.23: test_build_finger_data + * + * Build finger data and verify the tagged format. + * ================================================================ */ +static void +test_build_finger_data (void) +{ + gsize len; + guint8 template[] = { 0x11, 0x22, 0x33 }; + guint8 tid[] = { 0xAA, 0xBB }; + g_autofree guint8 *fd = validity_db_build_finger_data ( + 2, template, sizeof (template), tid, sizeof (tid), &len); + + g_assert_nonnull (fd); + + /* Check header: subtype(2) | flags=3(2) | tinfo_len(2) | 0x20(2) */ + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[0]), ==, 2); /* subtype */ + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[2]), ==, 3); /* flags */ + + gsize expected_tinfo_len = 4 + sizeof (template) + 4 + sizeof (tid); + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[4]), ==, expected_tinfo_len); + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[6]), ==, 0x20); + + /* Tag 1 (template) at offset 8 */ + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[8]), ==, 1); + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[10]), ==, sizeof (template)); + g_assert_cmpmem (&fd[12], sizeof (template), template, sizeof (template)); + + /* Tag 2 (tid) at offset 12+3 = 15 */ + gsize tid_offset = 12 + sizeof (template); + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[tid_offset]), ==, 2); + g_assert_cmpuint (FP_READ_UINT16_LE (&fd[tid_offset + 2]), ==, sizeof (tid)); + g_assert_cmpmem (&fd[tid_offset + 4], sizeof (tid), tid, sizeof (tid)); + + /* Total should be header(8) + tinfo + 0x20 padding */ + gsize expected_total = 8 + expected_tinfo_len + 0x20; + g_assert_cmpuint (len, ==, expected_total); +} + +/* ================================================================ + * T6.24: test_db_write_enable_blob + * + * Verify db_write_enable blob accessor returns a valid blob. + * ================================================================ */ +static void +test_db_write_enable_blob (void) +{ + gsize len; + + /* Test with 009a device type (known to have a 3621-byte blob) */ + const guint8 *blob = validity_db_get_write_enable_blob (VALIDITY_DEV_9A, &len); + + g_assert_nonnull (blob); + g_assert_cmpuint (len, >, 0); + g_assert_cmpuint (len, ==, 3621); + + /* Test all supported device types return valid blobs */ + const guint dev_types[] = { VALIDITY_DEV_90, VALIDITY_DEV_97, + VALIDITY_DEV_9A, VALIDITY_DEV_9D }; + for (guint i = 0; i < G_N_ELEMENTS (dev_types); i++) + { + gsize dbe_len; + const guint8 *dbe = validity_db_get_write_enable_blob (dev_types[i], &dbe_len); + g_assert_nonnull (dbe); + g_assert_cmpuint (dbe_len, >, 0); + } +} + +/* ================================================================ + * T6.25: test_parse_record_value + * + * Construct a record value response and verify parsing. + * ================================================================ */ +static void +test_parse_record_value (void) +{ + guint8 value[] = { 0x01, 0x02, 0x03 }; + /* Format: dbid(2) type(2) storage(2) sz(2) pad(2) value */ + gsize total = 10 + sizeof (value); + g_autofree guint8 *data = g_new0 (guint8, total); + + FP_WRITE_UINT16_LE (&data[0], 42); /* dbid */ + FP_WRITE_UINT16_LE (&data[2], 8); /* type = DATA */ + FP_WRITE_UINT16_LE (&data[4], 3); /* storage */ + FP_WRITE_UINT16_LE (&data[6], sizeof (value)); /* sz */ + FP_WRITE_UINT16_LE (&data[8], 0); /* pad */ + memcpy (&data[10], value, sizeof (value)); + + ValidityDbRecord record; + g_assert_true (validity_db_parse_record_value (data, total, &record)); + + g_assert_cmpuint (record.dbid, ==, 42); + g_assert_cmpuint (record.type, ==, 8); + g_assert_cmpuint (record.storage, ==, 3); + g_assert_nonnull (record.value); + g_assert_cmpuint (record.value_len, ==, sizeof (value)); + g_assert_cmpmem (record.value, record.value_len, value, sizeof (value)); + + validity_db_record_clear (&record); +} + +/* ================================================================ + * T6.26: test_parse_record_children + * + * Construct a record children response and verify parsing. + * ================================================================ */ +static void +test_parse_record_children (void) +{ + /* Format: dbid(2) type(2) storage(2) sz(2) cnt(2) pad(2) + * children[cnt * 4: dbid(2) type(2)] */ + gsize total = 12 + 2 * 4; + g_autofree guint8 *data = g_new0 (guint8, total); + + FP_WRITE_UINT16_LE (&data[0], 3); /* dbid */ + FP_WRITE_UINT16_LE (&data[2], 4); /* type = STORAGE */ + FP_WRITE_UINT16_LE (&data[4], 3); /* storage */ + FP_WRITE_UINT16_LE (&data[6], 0); /* sz */ + FP_WRITE_UINT16_LE (&data[8], 2); /* child_count */ + FP_WRITE_UINT16_LE (&data[10], 0); /* pad */ + + /* Children */ + FP_WRITE_UINT16_LE (&data[12], 10); /* child[0].dbid */ + FP_WRITE_UINT16_LE (&data[14], 5); /* child[0].type = USER */ + FP_WRITE_UINT16_LE (&data[16], 11); /* child[1].dbid */ + FP_WRITE_UINT16_LE (&data[18], 5); /* child[1].type = USER */ + + ValidityRecordChildren children; + g_assert_true (validity_db_parse_record_children (data, total, &children)); + + g_assert_cmpuint (children.dbid, ==, 3); + g_assert_cmpuint (children.type, ==, 4); + g_assert_cmpuint (children.storage, ==, 3); + g_assert_cmpuint (children.child_count, ==, 2); + g_assert_nonnull (children.children); + g_assert_cmpuint (children.children[0].dbid, ==, 10); + g_assert_cmpuint (children.children[0].type, ==, 5); + g_assert_cmpuint (children.children[1].dbid, ==, 11); + g_assert_cmpuint (children.children[1].type, ==, 5); + + validity_record_children_clear (&children); +} + +/* ================================================================ + * T6.27: test_cmd_enrollment_update + * + * Verify cmd 0x6B format with template data. + * ================================================================ */ +static void +test_cmd_enrollment_update (void) +{ + gsize len; + guint8 prev[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + g_autofree guint8 *cmd = validity_db_build_cmd_enrollment_update ( + prev, sizeof (prev), &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 1 + sizeof (prev)); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_ENROLLMENT_UPDATE); + g_assert_cmpmem (&cmd[1], sizeof (prev), prev, sizeof (prev)); +} + +/* ================================================================ + * T6.28: test_cmd_get_record_value + * + * Verify cmd 0x49 format. + * ================================================================ */ +static void +test_cmd_get_record_value (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_get_record_value (0x5678, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_RECORD_VALUE); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x5678); +} + +/* ================================================================ + * T6.29: test_cmd_get_record_children + * + * Verify cmd 0x46 format. + * ================================================================ */ +static void +test_cmd_get_record_children (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_get_record_children (0x1234, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_RECORD_CHILDREN); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x1234); +} + +/* ================================================================ + * Tests: VERIFY + * ================================================================ */ + + +/* ================================================================ + * Helper: build a TLV dict entry tag(2LE) | len(2LE) | data[len] + * Returns bytes written. + * ================================================================ */ +static gsize +build_tlv_entry (guint8 *buf, guint16 tag, const guint8 *data, guint16 len) +{ + FP_WRITE_UINT16_LE (&buf[0], tag); + FP_WRITE_UINT16_LE (&buf[2], len); + if (len > 0) + memcpy (&buf[4], data, len); + return 4 + len; +} + +/* ================================================================ + * Helper: Build a complete match result payload: + * total_len(2LE) | TLV entries... + * ================================================================ */ +static guint8 * +build_match_payload (guint32 user_dbid, + guint16 subtype, + const guint8 *hash, + gsize hash_len, + gsize *out_len) +{ + /* Max size: 2 (total_len) + 3 entries × (4 header + max data) */ + guint8 *buf = g_new0 (guint8, 256); + gsize pos = 2; /* skip total_len placeholder */ + + /* Tag 1: user_dbid (4 bytes LE) */ + guint8 dbid_data[4]; + + FP_WRITE_UINT32_LE (dbid_data, user_dbid); + pos += build_tlv_entry (&buf[pos], 1, dbid_data, 4); + + /* Tag 3: subtype (2 bytes LE) */ + guint8 sub_data[2]; + FP_WRITE_UINT16_LE (sub_data, subtype); + pos += build_tlv_entry (&buf[pos], 3, sub_data, 2); + + /* Tag 4: hash */ + if (hash && hash_len > 0) + pos += build_tlv_entry (&buf[pos], 4, hash, hash_len); + + /* Write total_len at offset 0 */ + FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); + + *out_len = pos; + return buf; +} + +/* ================================================================ + * R1: parse_match_result with valid TLV data + * + * Regression: Issue #1 — dead while loop would never extract fields. + * Verifies that user_dbid, subtype, and hash are correctly parsed + * from a TLV dictionary matching python-validity's parse_dict() format. + * ================================================================ */ +static void +test_parse_match_result_valid (void) +{ + guint8 hash[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; + gsize payload_len; + g_autofree guint8 *payload = build_match_payload ( + 0x00001234, 3, hash, sizeof (hash), &payload_len); + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (payload, payload_len, &result); + + g_assert_true (ok); + g_assert_true (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0x00001234); + g_assert_cmpuint (result.subtype, ==, 3); + g_assert_nonnull (result.hash); + g_assert_cmpuint (result.hash_len, ==, sizeof (hash)); + g_assert_cmpmem (result.hash, result.hash_len, hash, sizeof (hash)); + + validity_match_result_clear (&result); +} + +/* ================================================================ + * R1b: parse_match_result iterates ALL TLV entries + * + * Regression: The dead while loop would break after first entry. + * Build a dict with tag 3 (subtype) BEFORE tag 1 (user_dbid) to + * ensure the parser doesn't stop after the first entry. + * ================================================================ */ +static void +test_parse_match_result_multi_tags (void) +{ + /* Manually build: total_len(2) | tag3(2+2+2) | tag1(2+2+4) | tag4(2+2+3) */ + guint8 buf[256]; + gsize pos = 2; + + /* Tag 3 first: subtype = 7 */ + guint8 sub[2]; + + FP_WRITE_UINT16_LE (sub, 7); + pos += build_tlv_entry (&buf[pos], 3, sub, 2); + + /* Tag 1 second: user_dbid = 0xDEADBEEF */ + guint8 dbid[4]; + FP_WRITE_UINT32_LE (dbid, 0xDEADBEEF); + pos += build_tlv_entry (&buf[pos], 1, dbid, 4); + + /* Tag 4 third: hash = {0x11, 0x22, 0x33} */ + guint8 hash[] = { 0x11, 0x22, 0x33 }; + pos += build_tlv_entry (&buf[pos], 4, hash, 3); + + FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (buf, pos, &result); + + g_assert_true (ok); + g_assert_true (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0xDEADBEEF); + g_assert_cmpuint (result.subtype, ==, 7); + g_assert_nonnull (result.hash); + g_assert_cmpuint (result.hash_len, ==, 3); + g_assert_cmpmem (result.hash, result.hash_len, hash, 3); + + validity_match_result_clear (&result); +} + +/* ================================================================ + * R1c: parse_match_result with empty dict (no match) + * + * Regression: A no-match scenario should return ok=TRUE but matched=FALSE. + * ================================================================ */ +static void +test_parse_match_result_empty (void) +{ + /* total_len = 0, no TLV entries */ + guint8 buf[2] = { 0x00, 0x00 }; + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (buf, sizeof (buf), &result); + + g_assert_true (ok); + g_assert_false (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0); + g_assert_cmpuint (result.subtype, ==, 0); + g_assert_null (result.hash); +} + +/* ================================================================ + * R1d: parse_match_result with truncated data + * + * Ensure graceful handling of malformed/truncated payloads. + * ================================================================ */ +static void +test_parse_match_result_truncated (void) +{ + /* Only 1 byte — too short for total_len */ + guint8 buf1[1] = { 0x05 }; + ValidityMatchResult result = { 0 }; + + g_assert_false (validity_parse_match_result (buf1, 1, &result)); + + /* total_len says 20 but only 6 bytes follow (partial TLV entry) */ + guint8 buf2[8]; + FP_WRITE_UINT16_LE (&buf2[0], 20); + FP_WRITE_UINT16_LE (&buf2[2], 1); /* tag = 1 */ + FP_WRITE_UINT16_LE (&buf2[4], 10); /* len = 10, but only 2 bytes remain */ + buf2[6] = 0xFF; + buf2[7] = 0xFF; + + memset (&result, 0, sizeof (result)); + gboolean ok = validity_parse_match_result (buf2, sizeof (buf2), &result); + /* Should return TRUE (parsing succeeded) but matched=FALSE (incomplete entry) */ + g_assert_true (ok); + g_assert_false (result.matched); +} + +/* ================================================================ + * R1e: parse_match_result ignores unknown tags + * + * Unknown tags should be skipped without error. + * ================================================================ */ +static void +test_parse_match_result_unknown_tags (void) +{ + guint8 buf[256]; + gsize pos = 2; + + /* Unknown tag 99 with 2 bytes of data */ + guint8 unk[] = { 0x42, 0x43 }; + + pos += build_tlv_entry (&buf[pos], 99, unk, 2); + + /* Tag 1: user_dbid = 0x0042 */ + guint8 dbid[4]; + FP_WRITE_UINT32_LE (dbid, 0x0042); + pos += build_tlv_entry (&buf[pos], 1, dbid, 4); + + FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (buf, pos, &result); + + g_assert_true (ok); + g_assert_true (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0x0042); + + validity_match_result_clear (&result); +} + +/* ================================================================ + * R2: validity_db_build_identity rejects NULL + * + * Regression: Issue #2 — NULL user_id was passed to build_identity + * which would then be passed to g_variant_new_string(NULL) → crash. + * The guard should return NULL. + * ================================================================ */ +static void +test_build_identity_null (void) +{ + gsize len = 999; + + /* The function uses g_return_val_if_fail which emits g_critical. + * With G_DEBUG=fatal-warnings the critical would be fatal, + * so we must expect the message. */ + g_test_expect_message ("libfprint-validity", + G_LOG_LEVEL_CRITICAL, + "*assertion*uuid_str*failed*"); + + guint8 *id = validity_db_build_identity (NULL, &len); + g_test_assert_expected_messages (); + + g_assert_null (id); +} + +/* ================================================================ + * R2b: validity_db_build_identity with valid UUID + * + * Regression: Ensures UUID → identity bytes works correctly + * (complementary to the NULL test above). + * ================================================================ */ +static void +test_build_identity_valid_uuid (void) +{ + const gchar *uuid = "12345678-1234-5678-1234-567812345678"; + gsize len; + g_autofree guint8 *id = validity_db_build_identity (uuid, &len); + + g_assert_nonnull (id); + g_assert_cmpuint (len, >=, VALIDITY_IDENTITY_MIN_SIZE); + + /* Type should be SID (3) */ + g_assert_cmpuint (FP_READ_UINT32_LE (&id[0]), ==, VALIDITY_IDENTITY_TYPE_SID); + + /* Length field should be UUID string length */ + g_assert_cmpuint (FP_READ_UINT32_LE (&id[4]), ==, strlen (uuid)); + + /* UUID payload should be present */ + g_assert_cmpmem (&id[8], strlen (uuid), uuid, strlen (uuid)); +} + +/* ================================================================ + * R3: Gallery matching by subtype + * + * Regression: Issue #3 — identify always returned first gallery print + * regardless of actual subtype. Now it should match by finger subtype. + * ================================================================ */ +static void +test_gallery_match_by_subtype (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); + g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); + + /* Create 3 prints with fingers: LEFT_THUMB(1), LEFT_INDEX(2), RIGHT_MIDDLE(8) */ + FpPrint *p1 = fp_print_new (device); + fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); + g_ptr_array_add (gallery, g_object_ref_sink (p1)); + + FpPrint *p2 = fp_print_new (device); + fp_print_set_finger (p2, FP_FINGER_LEFT_INDEX); + g_ptr_array_add (gallery, g_object_ref_sink (p2)); + + FpPrint *p3 = fp_print_new (device); + fp_print_set_finger (p3, FP_FINGER_RIGHT_MIDDLE); + g_ptr_array_add (gallery, g_object_ref_sink (p3)); + + /* Subtype 2 = LEFT_INDEX → should match p2, not p1 */ + guint16 subtype_left_index = validity_finger_to_subtype (FP_FINGER_LEFT_INDEX); + FpPrint *match = validity_find_gallery_match (gallery, subtype_left_index); + g_assert_true (match == p2); + + /* Subtype 8 = RIGHT_MIDDLE → should match p3 */ + guint16 subtype_right_middle = validity_finger_to_subtype (FP_FINGER_RIGHT_MIDDLE); + match = validity_find_gallery_match (gallery, subtype_right_middle); + g_assert_true (match == p3); + + /* Subtype 1 = LEFT_THUMB → should match p1 */ + guint16 subtype_left_thumb = validity_finger_to_subtype (FP_FINGER_LEFT_THUMB); + match = validity_find_gallery_match (gallery, subtype_left_thumb); + g_assert_true (match == p1); +} + +/* ================================================================ + * R3b: Gallery match falls back to first when subtype doesn't match + * + * The sensor confirmed a match but the subtype can't be correlated + * to any gallery entry — should fall back to first. + * ================================================================ */ +static void +test_gallery_match_fallback (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); + g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); + + FpPrint *p1 = fp_print_new (device); + fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); + g_ptr_array_add (gallery, g_object_ref_sink (p1)); + + /* Subtype 9 = RIGHT_RING, not in gallery → should fall back to p1 */ + guint16 subtype_right_ring = validity_finger_to_subtype (FP_FINGER_RIGHT_RING); + FpPrint *match = validity_find_gallery_match (gallery, subtype_right_ring); + g_assert_true (match == p1); +} + +/* ================================================================ + * R3c: Gallery match with NULL/empty gallery + * + * Should return NULL when gallery is empty or NULL. + * ================================================================ */ +static void +test_gallery_match_empty (void) +{ + g_autoptr(GPtrArray) empty = g_ptr_array_new_with_free_func (g_object_unref); + + g_assert_null (validity_find_gallery_match (NULL, 1)); + g_assert_null (validity_find_gallery_match (empty, 1)); +} + +/* ================================================================ + * R4: enroll_user_dbid field exists separately from delete_storage_dbid + * + * Regression: Issue #4 — delete_storage_dbid was abused for enrollment. + * This compile-time test verifies both fields exist independently. + * ================================================================ */ +static void +test_struct_separate_fields (void) +{ + /* Verify both fields exist and are at different offsets */ + g_assert_cmpuint ( + G_STRUCT_OFFSET (FpiDeviceValidity, enroll_user_dbid), !=, + G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); + + /* Also verify delete_finger_subtype and delete_finger_dbid exist + * (needed for the functional delete SSM) */ + g_assert_cmpuint ( + G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_subtype), !=, + G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); + g_assert_cmpuint ( + G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_dbid), !=, + G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); +} + +/* ================================================================ + * R5: del_record command format + * + * Regression: Issue #5 — delete SSM never sent del_record cmd. + * Verify cmd 0x48 produces correct format: 0x48 | dbid(2LE). + * (This already exists in test-validity-db.c but we double-check + * the format critical for delete functionality.) + * ================================================================ */ +static void +test_del_record_format (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_del_record (0x4321, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DEL_RECORD); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x4321); +} + +/* ================================================================ + * R6: match_finger command is exactly 13 bytes (single allocation) + * + * Regression: Issue #6 — build_cmd_match_finger allocated 12 bytes, + * freed, then re-allocated 13 bytes. Now single allocation. + * ================================================================ */ +static void +test_match_finger_size (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_match_finger (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 13); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_FINGER); + g_assert_cmpuint (cmd[1], ==, 0x02); + g_assert_cmpuint (cmd[2], ==, 0xFF); + + /* Verify all 5 uint16_le fields */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); /* stg_id */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); /* usr_id */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, 1); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[9]), ==, 0); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[11]), ==, 0); +} + +/* ================================================================ + * R7: Clear storage SSM states exist + * + * Regression: Issue #7 — clear_storage was a stub returning NOT_SUPPORTED. + * Verify the CLEAR_* enum states exist (compile-time regression test). + * ================================================================ */ +static void +test_clear_storage_states_exist (void) +{ + /* Verify clear SSM states exist and are ordered correctly */ + g_assert_cmpint (CLEAR_GET_STORAGE, ==, 0); + g_assert_cmpint (CLEAR_GET_STORAGE_RECV, ==, 1); + g_assert_cmpint (CLEAR_DEL_USER, ==, 2); + g_assert_cmpint (CLEAR_DEL_USER_RECV, ==, 3); + g_assert_cmpint (CLEAR_DONE, ==, 4); + g_assert_cmpint (CLEAR_NUM_STATES, ==, 5); +} + +/* ================================================================ + * R7b: Delete SSM states are complete + * + * Verify the delete SSM has all required states including + * DEL_RECORD and DEL_RECORD_RECV (which were previously dead code). + * ================================================================ */ +static void +test_delete_states_exist (void) +{ + g_assert_cmpint (DELETE_GET_STORAGE, ==, 0); + g_assert_cmpint (DELETE_GET_STORAGE_RECV, ==, 1); + g_assert_cmpint (DELETE_LOOKUP_USER, ==, 2); + g_assert_cmpint (DELETE_LOOKUP_USER_RECV, ==, 3); + g_assert_cmpint (DELETE_DEL_RECORD, ==, 4); + g_assert_cmpint (DELETE_DEL_RECORD_RECV, ==, 5); + g_assert_cmpint (DELETE_DONE, ==, 6); + g_assert_cmpint (DELETE_NUM_STATES, ==, 7); +} + +/* ================================================================ + * R1f: match_result_clear frees hash + * + * Ensure the clear function properly frees the hash allocation. + * ================================================================ */ +static void +test_match_result_clear (void) +{ + ValidityMatchResult result = { 0 }; + + result.matched = TRUE; + result.user_dbid = 42; + result.subtype = 5; + result.hash = g_memdup2 ((guint8[]){0x01, 0x02}, 2); + result.hash_len = 2; + + validity_match_result_clear (&result); + + g_assert_false (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0); + g_assert_cmpuint (result.subtype, ==, 0); + g_assert_null (result.hash); + g_assert_cmpuint (result.hash_len, ==, 0); +} + +/* ================================================================ + * Tests: TLS + * ================================================================ */ + + +/* ================================================================ + * Test: PRF produces deterministic output + * ================================================================ */ +static void +test_prf_deterministic (void) +{ + guint8 secret[] = { 0x01, 0x02, 0x03, 0x04 }; + guint8 seed[] = { 0x05, 0x06, 0x07, 0x08 }; + guint8 output1[48]; + guint8 output2[48]; + + validity_tls_prf (secret, sizeof (secret), seed, sizeof (seed), + output1, sizeof (output1)); + validity_tls_prf (secret, sizeof (secret), seed, sizeof (seed), + output2, sizeof (output2)); + + g_assert_cmpmem (output1, sizeof (output1), output2, sizeof (output2)); +} + +/* ================================================================ + * Test: PRF with known TLS 1.2 test vector + * ================================================================ + * RFC 5246 does not define test vectors for SHA-256 PRF directly, + * but we verify our implementation against python-validity's output. + */ +static void +test_prf_output_length (void) +{ + guint8 secret[32]; + guint8 seed[64]; + guint8 output[0x120]; /* Same as key_block size */ + + memset (secret, 0xAB, sizeof (secret)); + memset (seed, 0xCD, sizeof (seed)); + + validity_tls_prf (secret, sizeof (secret), seed, sizeof (seed), + output, sizeof (output)); + + /* PRF output should not be all zeros */ + gboolean all_zero = TRUE; + for (gsize i = 0; i < sizeof (output); i++) + { + if (output[i] != 0) + { + all_zero = FALSE; + break; + } + } + g_assert_false (all_zero); +} + +/* ================================================================ + * Test: PRF with different lengths uses correct number of HMAC iters + * ================================================================ */ +static void +test_prf_short_output (void) +{ + guint8 secret[] = { 0x01 }; + guint8 seed[] = { 0x02 }; + guint8 output_short[16]; + guint8 output_long[48]; + + validity_tls_prf (secret, 1, seed, 1, output_short, sizeof (output_short)); + validity_tls_prf (secret, 1, seed, 1, output_long, sizeof (output_long)); + + /* First 16 bytes should match */ + g_assert_cmpmem (output_short, 16, output_long, 16); +} + +/* ================================================================ + * Test: Encrypt then decrypt roundtrip + * ================================================================ */ +static void +test_encrypt_decrypt_roundtrip (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + /* Set up encryption/decryption keys (same for roundtrip test) */ + memset (tls.encryption_key, 0x42, TLS_AES_KEY_SIZE); + memset (tls.decryption_key, 0x42, TLS_AES_KEY_SIZE); + + guint8 plaintext[] = "Hello, TLS! This is a test message for encryption."; + gsize pt_len = sizeof (plaintext); + + gsize enc_len; + guint8 *encrypted = validity_tls_encrypt (&tls, plaintext, pt_len, &enc_len); + g_assert_nonnull (encrypted); + g_assert_cmpuint (enc_len, >, pt_len); /* IV + padded ciphertext */ + + GError *error = NULL; + gsize dec_len; + guint8 *decrypted = validity_tls_decrypt (&tls, encrypted, enc_len, + &dec_len, &error); + g_assert_no_error (error); + g_assert_nonnull (decrypted); + g_assert_cmpmem (plaintext, pt_len, decrypted, dec_len); + + g_free (encrypted); + g_free (decrypted); + validity_tls_free (&tls); +} + +/* ================================================================ + * Test: Encrypt with block-aligned data + * ================================================================ */ +static void +test_encrypt_block_aligned (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + memset (tls.encryption_key, 0x55, TLS_AES_KEY_SIZE); + memset (tls.decryption_key, 0x55, TLS_AES_KEY_SIZE); + + /* 16 bytes = exactly one AES block */ + guint8 plaintext[16]; + memset (plaintext, 0xAA, 16); + + gsize enc_len; + guint8 *encrypted = validity_tls_encrypt (&tls, plaintext, 16, &enc_len); + g_assert_nonnull (encrypted); + /* Should be IV(16) + 32 bytes (16 data + 16 padding since pad=0x0f*16) */ + g_assert_cmpuint (enc_len, ==, 16 + 32); + + GError *error = NULL; + gsize dec_len; + guint8 *decrypted = validity_tls_decrypt (&tls, encrypted, enc_len, + &dec_len, &error); + g_assert_no_error (error); + g_assert_nonnull (decrypted); + g_assert_cmpuint (dec_len, ==, 16); + g_assert_cmpmem (plaintext, 16, decrypted, 16); + + g_free (encrypted); + g_free (decrypted); + validity_tls_free (&tls); +} + +/* ================================================================ + * Test: Decrypt with invalid data fails + * ================================================================ */ +static void +test_decrypt_invalid (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + memset (tls.decryption_key, 0x55, TLS_AES_KEY_SIZE); + + /* Too short for IV + block */ + guint8 short_data[10]; + memset (short_data, 0, sizeof (short_data)); + + GError *error = NULL; + gsize dec_len; + guint8 *decrypted = validity_tls_decrypt (&tls, short_data, + sizeof (short_data), + &dec_len, &error); + g_assert_null (decrypted); + g_assert_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_PROTO); + g_clear_error (&error); + + validity_tls_free (&tls); +} + +/* ================================================================ + * Test: PSK derivation runs without crashing + * ================================================================ */ +static void +test_psk_derivation (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + validity_tls_derive_psk (&tls); + + /* PSK keys should not be all zeros */ + gboolean all_zero = TRUE; + for (gsize i = 0; i < TLS_AES_KEY_SIZE; i++) + { + if (tls.psk_encryption_key[i] != 0) + { + all_zero = FALSE; + break; + } + } + g_assert_false (all_zero); + + all_zero = TRUE; + for (gsize i = 0; i < TLS_AES_KEY_SIZE; i++) + { + if (tls.psk_validation_key[i] != 0) + { + all_zero = FALSE; + break; + } + } + g_assert_false (all_zero); + + validity_tls_free (&tls); +} + +/* ================================================================ + * Test: PSK derivation is deterministic + * ================================================================ */ +static void +test_psk_deterministic (void) +{ + ValidityTlsState tls1, tls2; + + validity_tls_init (&tls1); + validity_tls_init (&tls2); + + validity_tls_derive_psk (&tls1); + validity_tls_derive_psk (&tls2); + + g_assert_cmpmem (tls1.psk_encryption_key, TLS_AES_KEY_SIZE, + tls2.psk_encryption_key, TLS_AES_KEY_SIZE); + g_assert_cmpmem (tls1.psk_validation_key, TLS_AES_KEY_SIZE, + tls2.psk_validation_key, TLS_AES_KEY_SIZE); + + validity_tls_free (&tls1); + validity_tls_free (&tls2); +} + +/* ================================================================ + * Test: Flash parse with empty data fails gracefully + * ================================================================ */ +static void +test_flash_parse_empty (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + GError *error = NULL; + guint8 empty_flash[] = { 0xFF, 0xFF, 0x00, 0x00 }; /* end block */ + + /* Flash with only end marker → missing keys */ + gboolean result = validity_tls_parse_flash (&tls, empty_flash, + sizeof (empty_flash), + &error); + g_assert_false (result); + g_assert_nonnull (error); + g_assert_false (tls.keys_loaded); + + g_clear_error (&error); + validity_tls_free (&tls); +} + +/* ================================================================ + * Test: Flash parse with truncated data fails gracefully + * ================================================================ */ +static void +test_flash_parse_truncated (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + GError *error = NULL; + guint8 truncated[] = { 0x03, 0x00, 0xFF, 0x00 }; /* cert block w/ impossibly large size */ + + gboolean result = validity_tls_parse_flash (&tls, truncated, + sizeof (truncated), + &error); + /* Should fail due to block size exceeding remaining data */ + g_assert_false (result); + g_assert_nonnull (error); + g_clear_error (&error); + + validity_tls_free (&tls); +} + +/* ================================================================ + * Test: Init/free cycle doesn't leak + * ================================================================ */ +static void +test_init_free (void) +{ + ValidityTlsState tls; + + for (int i = 0; i < 10; i++) + { + validity_tls_init (&tls); + validity_tls_free (&tls); + } +} + +/* ================================================================ + * Test: Build ClientHello produces valid TLS record + * ================================================================ */ +static void +test_build_client_hello (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + gsize out_len; + guint8 *hello = validity_tls_build_client_hello (&tls, &out_len); + + g_assert_nonnull (hello); + g_assert_cmpuint (out_len, >, 4 + 5); /* prefix(4) + record header(5) minimum */ + + /* Check prefix: 0x44 0x00 0x00 0x00 */ + g_assert_cmpint (hello[0], ==, 0x44); + g_assert_cmpint (hello[1], ==, 0x00); + g_assert_cmpint (hello[2], ==, 0x00); + g_assert_cmpint (hello[3], ==, 0x00); + + /* Check TLS record header */ + g_assert_cmpint (hello[4], ==, 0x16); /* handshake */ + g_assert_cmpint (hello[5], ==, 0x03); /* version major */ + g_assert_cmpint (hello[6], ==, 0x03); /* version minor */ + + /* client_random should have been set */ + gboolean has_random = FALSE; + for (gsize i = 0; i < TLS_RANDOM_SIZE; i++) + { + if (tls.client_random[i] != 0) + { + has_random = TRUE; + break; + } + } + g_assert_true (has_random); + + g_free (hello); + validity_tls_free (&tls); +} + +/* ================================================================ + * Test: Wrap/unwrap with invalid data fails gracefully + * ================================================================ */ +static void +test_unwrap_invalid (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + GError *error = NULL; + gsize out_len; + + /* Short data → truncated record header */ + guint8 short_data[] = { 0x17, 0x03 }; + guint8 *result = validity_tls_unwrap_response (&tls, short_data, + sizeof (short_data), + &out_len, &error); + g_assert_null (result); + g_assert_nonnull (error); + g_clear_error (&error); + + /* App data before secure channel */ + guint8 app_early[] = { 0x17, 0x03, 0x03, 0x00, 0x10, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; + result = validity_tls_unwrap_response (&tls, app_early, + sizeof (app_early), + &out_len, &error); + g_assert_null (result); + g_assert_nonnull (error); + g_clear_error (&error); + + validity_tls_free (&tls); +} + +/* ================================================================ + * Regression: Bug #1 — Flash parse requires PSK for private key + * + * Private key block (ID 4) is encrypted with PSK. Calling parse_flash + * without first deriving PSK must fail (HMAC mismatch), proving the + * ordering dependency. This catches the bug where flash_read SSM + * parsed flash data BEFORE PSK derivation had occurred. + * ================================================================ */ +static void +test_flash_parse_needs_psk (void) +{ + ValidityTlsState tls_with_psk, tls_no_psk; + + validity_tls_init (&tls_with_psk); + validity_tls_init (&tls_no_psk); + + /* Derive PSK so we can build a valid encrypted private key block */ + validity_tls_derive_psk (&tls_with_psk); + + /* Build a realistic flash image with a cert block + encrypted privkey block. + * We use a minimal cert (just 16 bytes of dummy data) and a privkey block + * that's encrypted with the proper PSK. */ + + /* Step 1: Build a cert body */ + guint8 cert_body[16]; + memset (cert_body, 0xAA, sizeof (cert_body)); + + /* Step 2: Build a private-key body encrypted with PSK */ + guint8 priv_plaintext[96]; /* d(32) + pad for block alignment */ + memset (priv_plaintext, 0xBB, sizeof (priv_plaintext)); + + /* Encrypt plaintext with PSK encryption key */ + guint8 iv[TLS_IV_SIZE]; + memset (iv, 0x11, TLS_IV_SIZE); + gsize ct_len = sizeof (priv_plaintext); + guint8 *ciphertext = g_malloc (TLS_IV_SIZE + ct_len); + memcpy (ciphertext, iv, TLS_IV_SIZE); + + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new (); + int out_len, final_len; + EVP_EncryptInit_ex (ctx, EVP_aes_256_cbc (), NULL, + tls_with_psk.psk_encryption_key, iv); + EVP_CIPHER_CTX_set_padding (ctx, 0); + EVP_EncryptUpdate (ctx, ciphertext + TLS_IV_SIZE, &out_len, + priv_plaintext, ct_len); + EVP_EncryptFinal_ex (ctx, ciphertext + TLS_IV_SIZE + out_len, &final_len); + EVP_CIPHER_CTX_free (ctx); + gsize enc_total = TLS_IV_SIZE + ct_len; + + /* HMAC over (iv + ciphertext) with psk_validation_key */ + guint8 mac[TLS_HMAC_SIZE]; + unsigned int mac_len; + HMAC (EVP_sha256 (), + tls_with_psk.psk_validation_key, TLS_AES_KEY_SIZE, + ciphertext, enc_total, mac, &mac_len); + + /* Private key block payload: 0x02 || ciphertext || hmac */ + gsize priv_block_len = 1 + enc_total + TLS_HMAC_SIZE; + guint8 *priv_block = g_malloc (priv_block_len); + priv_block[0] = 0x02; + memcpy (priv_block + 1, ciphertext, enc_total); + memcpy (priv_block + 1 + enc_total, mac, TLS_HMAC_SIZE); + g_free (ciphertext); + + /* Build flash image: [cert_header][cert_body][priv_header][priv_body][end] */ + GByteArray *flash = g_byte_array_new (); + + /* Cert block header: id=0x0003, size, sha256 hash */ + guint8 cert_hdr[TLS_FLASH_BLOCK_HEADER_SIZE]; + FP_WRITE_UINT16_LE (cert_hdr, TLS_FLASH_BLOCK_CERT); + FP_WRITE_UINT16_LE (cert_hdr + 2, sizeof (cert_body)); + GChecksum *cs = g_checksum_new (G_CHECKSUM_SHA256); + gsize hash_len = 32; + g_checksum_update (cs, cert_body, sizeof (cert_body)); + g_checksum_get_digest (cs, cert_hdr + 4, &hash_len); + g_checksum_free (cs); + g_byte_array_append (flash, cert_hdr, sizeof (cert_hdr)); + g_byte_array_append (flash, cert_body, sizeof (cert_body)); + + /* Priv block header */ + guint8 priv_hdr[TLS_FLASH_BLOCK_HEADER_SIZE]; + FP_WRITE_UINT16_LE (priv_hdr, TLS_FLASH_BLOCK_PRIVKEY); + FP_WRITE_UINT16_LE (priv_hdr + 2, priv_block_len); + cs = g_checksum_new (G_CHECKSUM_SHA256); + hash_len = 32; + g_checksum_update (cs, priv_block, priv_block_len); + g_checksum_get_digest (cs, priv_hdr + 4, &hash_len); + g_checksum_free (cs); + g_byte_array_append (flash, priv_hdr, sizeof (priv_hdr)); + g_byte_array_append (flash, priv_block, priv_block_len); + + /* End marker */ + guint8 end_marker[4] = { 0xFF, 0xFF, 0x00, 0x00 }; + g_byte_array_append (flash, end_marker, sizeof (end_marker)); + + /* TEST: Without PSK, parse_flash must fail on the privkey block */ + GError *error = NULL; + gboolean result = validity_tls_parse_flash (&tls_no_psk, + flash->data, flash->len, + &error); + g_assert_false (result); + g_assert_nonnull (error); + /* Should fail with HMAC-related error since PSK is all zeros */ + g_clear_error (&error); + + g_byte_array_free (flash, TRUE); + g_free (priv_block); + validity_tls_free (&tls_with_psk); + validity_tls_free (&tls_no_psk); +} + +/* ================================================================ + * Regression: Bug #2 — READ_FLASH command format + * + * The READ_FLASH command must be exactly 13 bytes matching + * python-validity: pack('message, "TLS flash: incomplete key data")); + g_clear_error (&error); + validity_tls_free (&tls); + + /* Verify the bug scenario: passing the raw response (with the 6-byte + * header) gives DIFFERENT data to the parser than the correctly unwrapped + * payload. The first 4 bytes of the raw response are the LE size field + * (0x04 0x00 0x00 0x00), which would be misinterpreted as block_id=0x0004 + * (PRIVKEY block with size 0). This is a data corruption — the parser + * receives wrong input either way, but the key point is that the raw + * response and the unwrapped payload are NOT the same buffer content. */ + g_assert_cmpuint (sizeof (response), !=, payload_len); + g_assert_true (memcmp (response, payload, payload_len) != 0); +} + +/* ================================================================ + * Regression: Bug #4 — TLS handshake expects raw TLS records + * + * parse_server_hello expects raw TLS records starting with a content + * type byte (0x16 for Handshake). The old code used vcsfw_cmd_send + * which strips 2 bytes of VCSFW status, corrupting the TLS record. + * This test verifies that: + * - A valid TLS Handshake record header is accepted + * - Data prefixed with a 2-byte VCSFW status is rejected + * ================================================================ */ +static void +test_server_hello_rejects_vcsfw_prefix (void) +{ + /* Build a minimal valid TLS ServerHello record */ + guint8 server_hello_msg[] = { + /* Handshake message: ServerHello (type 0x02) */ + 0x02, /* type: ServerHello */ + 0x00, 0x00, 0x26, /* length: 38 bytes */ + 0x03, 0x03, /* version 1.2 */ + /* 32 bytes server_random */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x00, /* session_id length: 0 */ + 0xC0, 0x05, /* cipher suite: 0xC005 */ + 0x00, /* compression: none */ + }; + + gsize hs_len = sizeof (server_hello_msg); + + /* Wrap in TLS record: content_type(1) + version(2) + length(2) + body */ + gsize raw_tls_len = 5 + hs_len; + guint8 *raw_tls = g_malloc (raw_tls_len); + + raw_tls[0] = TLS_CONTENT_HANDSHAKE; /* 0x16 */ + raw_tls[1] = TLS_VERSION_MAJOR; + raw_tls[2] = TLS_VERSION_MINOR; + raw_tls[3] = (hs_len >> 8) & 0xff; + raw_tls[4] = hs_len & 0xff; + memcpy (raw_tls + 5, server_hello_msg, hs_len); + + /* Test 1: parse_server_hello with raw TLS — should succeed */ + ValidityTlsState tls; + validity_tls_init (&tls); + tls.handshake_hash = g_checksum_new (G_CHECKSUM_SHA256); + GError *error = NULL; + + gboolean result = validity_tls_parse_server_hello (&tls, raw_tls, + raw_tls_len, &error); + g_assert_no_error (error); + g_assert_true (result); + /* Verify server_random was properly extracted */ + g_assert_cmpint (tls.server_random[0], ==, 0x01); + g_assert_cmpint (tls.server_random[31], ==, 0x20); + validity_tls_free (&tls); + + /* Test 2: Prepend a 2-byte VCSFW status (0x0000) — simulates what + * vcsfw_cmd_send's cmd_receive_cb would have already STRIPPED. + * But if the raw recv path is wrong and doesn't strip, the parser + * gets [0x00, 0x00, 0x16, ...] — first byte 0x00 is not a valid + * TLS content type, so parsing should behave differently. */ + gsize prefixed_len = 2 + raw_tls_len; + guint8 *prefixed = g_malloc (prefixed_len); + prefixed[0] = 0x00; /* VCSFW status lo */ + prefixed[1] = 0x00; /* VCSFW status hi */ + memcpy (prefixed + 2, raw_tls, raw_tls_len); + + validity_tls_init (&tls); + tls.handshake_hash = g_checksum_new (G_CHECKSUM_SHA256); + + result = validity_tls_parse_server_hello (&tls, prefixed, prefixed_len, + &error); + /* With the 2-byte prefix, the first "record" starts at byte 0: + * content_type=0x00 is NOT TLS_CONTENT_HANDSHAKE (0x16), so the + * parser treats it as unknown content and either fails or skips it, + * and the server_random will NOT match the expected values. */ + if (result) + { + /* Even if parsing didn't error, server_random should be wrong */ + gboolean random_ok = (tls.server_random[0] == 0x01 && + tls.server_random[31] == 0x20); + g_assert_false (random_ok); + } + g_clear_error (&error); + validity_tls_free (&tls); + + g_free (raw_tls); + g_free (prefixed); +} + +/* ================================================================ + * Regression: Bug #5 — Client hello has 0x44 prefix (not VCSFW cmd) + * + * TLS handshake messages use 0x44000000 as a 4-byte prefix, NOT a + * standard VCSFW command byte. This test verifies the prefix and that + * the TLS record immediately follows (no VCSFW status expected in + * response). + * ================================================================ */ +static void +test_client_hello_tls_prefix (void) +{ + ValidityTlsState tls; + + validity_tls_init (&tls); + + gsize out_len; + guint8 *hello = validity_tls_build_client_hello (&tls, &out_len); + g_assert_nonnull (hello); + + /* Must start with 0x44 0x00 0x00 0x00 (TLS prefix, not VCSFW) */ + g_assert_cmpint (hello[0], ==, 0x44); + g_assert_cmpint (hello[1], ==, 0x00); + g_assert_cmpint (hello[2], ==, 0x00); + g_assert_cmpint (hello[3], ==, 0x00); + + /* Byte 4 must be TLS Handshake content type (0x16) */ + g_assert_cmpint (hello[4], ==, TLS_CONTENT_HANDSHAKE); + + /* Bytes 5-6 must be TLS version 1.2 (0x0303) */ + g_assert_cmpint (hello[5], ==, TLS_VERSION_MAJOR); + g_assert_cmpint (hello[6], ==, TLS_VERSION_MINOR); + + /* The prefix (0x44) must NOT equal any VCSFW command byte. + * Specifically, 0x44 != VCSFW_CMD_READ_FLASH (0x40) and + * is not any known VCSFW command. This proves TLS messages + * travel on a separate "channel". */ + g_assert_cmpint (hello[0], !=, VCSFW_CMD_GET_VERSION); + g_assert_cmpint (hello[0], !=, VCSFW_CMD_READ_FLASH); + g_assert_cmpint (hello[0], !=, VCSFW_CMD_GET_FW_INFO); + + g_free (hello); + validity_tls_free (&tls); +} + +/* ================================================================ + * Main + * ================================================================ */ + +/* ================================================================ + * Tests: PAIR + * ================================================================ */ + + +/* ================================================================ + * T7.11: parse_flash_info — valid response + * ================================================================ */ +static void +test_parse_flash_info_valid (void) +{ + /* CMD 0x3e response format (after 2-byte status, already stripped): + * [jid0:2LE][jid1:2LE][blocks:2LE][unknown0:2LE][blocksize:2LE] + * [unknown1:2LE][pcnt:2LE] = 14 bytes minimum */ + guint8 data[14]; + + memset (data, 0, sizeof (data)); + + /* jid0=0x01, jid1=0x02, blocks=0x1000, unknown0=0, blocksize=0x100, + * unknown1=0, pcnt=5 (5 partitions = device already paired) */ + FP_WRITE_UINT16_LE (data + 0, 0x0001); /* jid0 */ + FP_WRITE_UINT16_LE (data + 2, 0x0002); /* jid1 */ + FP_WRITE_UINT16_LE (data + 4, 0x1000); /* blocks */ + FP_WRITE_UINT16_LE (data + 6, 0x0000); /* unknown0 */ + FP_WRITE_UINT16_LE (data + 8, 0x0100); /* blocksize */ + FP_WRITE_UINT16_LE (data + 10, 0x0000); /* unknown1 */ + FP_WRITE_UINT16_LE (data + 12, 5); /* pcnt = 5 */ + + ValidityFlashIcParams ic; + guint16 num_partitions; + + gboolean ok = validity_pair_parse_flash_info (data, sizeof (data), + &ic, &num_partitions); + g_assert_true (ok); + g_assert_cmpuint (num_partitions, ==, 5); + g_assert_cmpuint (ic.size, ==, 0x1000 * 0x0100); + g_assert_cmpuint (ic.sector_size, ==, 0x1000); + g_assert_cmpuint (ic.sector_erase_cmd, ==, 0x20); +} + +/* ================================================================ + * T7.12: parse_flash_info — zero partitions means needs pairing + * ================================================================ */ +static void +test_parse_flash_info_needs_pairing (void) +{ + guint8 data[14]; + + memset (data, 0, sizeof (data)); + + FP_WRITE_UINT16_LE (data + 0, 0x0001); + FP_WRITE_UINT16_LE (data + 2, 0x0002); + FP_WRITE_UINT16_LE (data + 4, 0x0800); + FP_WRITE_UINT16_LE (data + 8, 0x0200); + FP_WRITE_UINT16_LE (data + 12, 0); /* 0 partitions */ + + ValidityFlashIcParams ic; + guint16 num_partitions; + + gboolean ok = validity_pair_parse_flash_info (data, sizeof (data), + &ic, &num_partitions); + g_assert_true (ok); + g_assert_cmpuint (num_partitions, ==, 0); +} + +/* ================================================================ + * T7.13: parse_flash_info — too short data fails + * ================================================================ */ +static void +test_parse_flash_info_too_short (void) +{ + guint8 data[10]; /* less than 14 bytes */ + + memset (data, 0, sizeof (data)); + + ValidityFlashIcParams ic; + guint16 num_partitions; + + gboolean ok = validity_pair_parse_flash_info (data, sizeof (data), + &ic, &num_partitions); + g_assert_false (ok); +} + +/* ================================================================ + * T7.14: serialize_partition — known output format + * ================================================================ */ +static void +test_serialize_partition (void) +{ + ValidityPartition part = { + .id = 1, + .type = 3, + .access_lvl = 0x0002, + .offset = 0x1000, + .size = 0x8000, + }; + + guint8 out[VALIDITY_PARTITION_ENTRY_SIZE]; + + validity_pair_serialize_partition (&part, out); + + /* Check first 12 bytes: id(1) type(1) access_lvl(2LE) offset(4LE) size(4LE) */ + g_assert_cmpuint (out[0], ==, 1); + g_assert_cmpuint (out[1], ==, 3); + g_assert_cmpuint (FP_READ_UINT16_LE (out + 2), ==, 0x0002); + g_assert_cmpuint (FP_READ_UINT32_LE (out + 4), ==, 0x1000); + g_assert_cmpuint (FP_READ_UINT32_LE (out + 8), ==, 0x8000); + + /* Bytes 12-15 should be zero */ + for (int i = 12; i < 16; i++) + g_assert_cmpuint (out[i], ==, 0); + + /* Bytes 16-47 = SHA-256 of the 12-byte entry */ + g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA256); + g_checksum_update (checksum, out, 12); + guint8 expected_hash[32]; + gsize hash_len = 32; + g_checksum_get_digest (checksum, expected_hash, &hash_len); + + g_assert_cmpmem (out + 16, 32, expected_hash, 32); + + /* Total size must be VALIDITY_PARTITION_ENTRY_SIZE */ + g_assert_cmpuint (sizeof (out), ==, VALIDITY_PARTITION_ENTRY_SIZE); +} + +/* ================================================================ + * T7.15: make_cert — produces 444-byte certificate + * ================================================================ */ +static void +test_make_cert_size (void) +{ + guint8 pub_x[32], pub_y[32]; + + RAND_bytes (pub_x, 32); + RAND_bytes (pub_y, 32); + + gsize cert_len; + g_autofree guint8 *cert = validity_pair_make_cert (pub_x, pub_y, &cert_len); + + g_assert_nonnull (cert); + g_assert_cmpuint (cert_len, ==, VALIDITY_CLIENT_CERT_SIZE); + + /* First 4 bytes should be 0x17 in LE */ + g_assert_cmpuint (FP_READ_UINT32_LE (cert), ==, 0x17); + /* Bytes 4-7 should be 0x20 in LE */ + g_assert_cmpuint (FP_READ_UINT32_LE (cert + 4), ==, 0x20); +} + +/* ================================================================ + * T7.16: make_cert — deterministic for same input + * ================================================================ */ +static void +test_make_cert_deterministic (void) +{ + /* Using fixed keys so signature is reproducible. + * Note: ECDSA uses random k, so signatures differ — but the + * structure should be consistent. Actually ECDSA is NOT deterministic + * without RFC 6979, so we can only verify structure matches. */ + guint8 pub_x[32] = { 0x01 }; + guint8 pub_y[32] = { 0x02 }; + + gsize len1, len2; + g_autofree guint8 *cert1 = validity_pair_make_cert (pub_x, pub_y, &len1); + g_autofree guint8 *cert2 = validity_pair_make_cert (pub_x, pub_y, &len2); + + g_assert_nonnull (cert1); + g_assert_nonnull (cert2); + g_assert_cmpuint (len1, ==, len2); + g_assert_cmpuint (len1, ==, VALIDITY_CLIENT_CERT_SIZE); + + /* Header and public key portion should be identical (first 184 bytes = cert body) */ + g_assert_cmpmem (cert1, 184, cert2, 184); +} + +/* ================================================================ + * T7.17: encrypt_key — output blob has correct structure + * ================================================================ */ +static void +test_encrypt_key_structure (void) +{ + guint8 priv[32], pub_x[32], pub_y[32]; + guint8 enc_key[32], val_key[32]; + + RAND_bytes (priv, 32); + RAND_bytes (pub_x, 32); + RAND_bytes (pub_y, 32); + RAND_bytes (enc_key, 32); + RAND_bytes (val_key, 32); + + gsize blob_len; + g_autofree guint8 *blob = validity_pair_encrypt_key (priv, pub_x, pub_y, + enc_key, val_key, + &blob_len); + + g_assert_nonnull (blob); + /* Blob format: 0x02(1) + IV(16) + ciphertext(112) + HMAC(32) = 161 */ + g_assert_cmpuint (blob_len, ==, 161); + g_assert_cmpuint (blob[0], ==, VALIDITY_ENCRYPTED_KEY_PREFIX); +} + +/* ================================================================ + * T7.18: encrypt_key — HMAC verification + * ================================================================ */ +static void +test_encrypt_key_hmac_valid (void) +{ + guint8 priv[32], pub_x[32], pub_y[32]; + guint8 enc_key[32], val_key[32]; + + RAND_bytes (priv, 32); + RAND_bytes (pub_x, 32); + RAND_bytes (pub_y, 32); + RAND_bytes (enc_key, 32); + RAND_bytes (val_key, 32); + + gsize blob_len; + g_autofree guint8 *blob = validity_pair_encrypt_key (priv, pub_x, pub_y, + enc_key, val_key, + &blob_len); + + g_assert_nonnull (blob); + g_assert_cmpuint (blob_len, ==, 161); + + /* Blob: [0x02][iv:16][ct:112][hmac:32] + * HMAC is over iv+ct (bytes 1..128) */ + const guint8 *iv_ct = blob + 1; + gsize iv_ct_len = 16 + 112; + const guint8 *stored_hmac = blob + 1 + iv_ct_len; + + guint8 computed_hmac[32]; + guint hmac_len = 32; + HMAC (EVP_sha256 (), val_key, 32, iv_ct, iv_ct_len, + computed_hmac, &hmac_len); + + g_assert_cmpmem (stored_hmac, 32, computed_hmac, 32); +} + +/* ================================================================ + * T7.19: build_partition_flash_cmd — valid output structure + * ================================================================ */ +static void +test_build_partition_flash_cmd (void) +{ + /* Use a real device descriptor for the flash layout */ + const ValidityDeviceDesc *desc = + validity_hal_device_lookup (VALIDITY_DEV_9A); + + g_assert_nonnull (desc); + + ValidityFlashIcParams flash_ic = { + .size = 0x200000, + .sector_size = 0x1000, + .sector_erase_cmd = 0x20, + }; + + guint8 pub_x[32], pub_y[32]; + RAND_bytes (pub_x, 32); + RAND_bytes (pub_y, 32); + + gsize cmd_len; + g_autofree guint8 *cmd = + validity_pair_build_partition_flash_cmd (&flash_ic, desc->flash_layout, + pub_x, pub_y, &cmd_len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (cmd_len, >, 5); + + /* Command prefix: 0x4f followed by 4 zero bytes */ + g_assert_cmpuint (cmd[0], ==, 0x4f); + g_assert_cmpuint (cmd[1], ==, 0); + g_assert_cmpuint (cmd[2], ==, 0); + g_assert_cmpuint (cmd[3], ==, 0); + g_assert_cmpuint (cmd[4], ==, 0); +} + +/* ================================================================ + * T7.20: build_tls_flash — produces exactly 4096 bytes + * ================================================================ */ +static void +test_build_tls_flash_size (void) +{ + ValidityPairState state; + + validity_pair_state_init (&state); + + /* Set up minimal test data */ + guint8 priv_blob[100]; + guint8 server_cert[200]; + guint8 ecdh_blob[400]; + RAND_bytes (priv_blob, sizeof (priv_blob)); + RAND_bytes (server_cert, sizeof (server_cert)); + RAND_bytes (ecdh_blob, sizeof (ecdh_blob)); + + state.priv_blob = priv_blob; + state.priv_blob_len = sizeof (priv_blob); + state.server_cert = server_cert; + state.server_cert_len = sizeof (server_cert); + state.ecdh_blob = ecdh_blob; + state.ecdh_blob_len = sizeof (ecdh_blob); + + gsize flash_len; + g_autofree guint8 *flash = validity_pair_build_tls_flash (&state, &flash_len); + + g_assert_nonnull (flash); + g_assert_cmpuint (flash_len, ==, 0x1000); + + /* Verify padding bytes at end are 0xff */ + gboolean has_ff_padding = FALSE; + for (gsize i = flash_len - 1; i > 0; i--) + { + if (flash[i] == 0xff) + { + has_ff_padding = TRUE; + break; + } + } + g_assert_true (has_ff_padding); + + /* Don't free embedded pointers since they're stack-allocated */ + state.priv_blob = NULL; + state.server_cert = NULL; + state.ecdh_blob = NULL; + validity_pair_state_free (&state); +} + +/* ================================================================ + * T7.21: build_tls_flash — block structure + * ================================================================ */ +static void +test_build_tls_flash_blocks (void) +{ + ValidityPairState state; + + validity_pair_state_init (&state); + + guint8 priv_blob[50]; + guint8 server_cert[100]; + guint8 ecdh_blob[400]; + memset (priv_blob, 0xAA, sizeof (priv_blob)); + memset (server_cert, 0xBB, sizeof (server_cert)); + memset (ecdh_blob, 0xCC, sizeof (ecdh_blob)); + + state.priv_blob = priv_blob; + state.priv_blob_len = sizeof (priv_blob); + state.server_cert = server_cert; + state.server_cert_len = sizeof (server_cert); + state.ecdh_blob = ecdh_blob; + state.ecdh_blob_len = sizeof (ecdh_blob); + + gsize flash_len; + g_autofree guint8 *flash = validity_pair_build_tls_flash (&state, &flash_len); + g_assert_nonnull (flash); + + /* Block 0 should be first: id=0, size=1 */ + g_assert_cmpuint (FP_READ_UINT16_LE (flash), ==, 0); /* block id */ + g_assert_cmpuint (FP_READ_UINT16_LE (flash + 2), ==, 1); /* size = 1 */ + /* Skip 32-byte hash at flash+4 and 1-byte body at flash+36 */ + + /* Next block should be block 4 (priv_blob) at offset 37 */ + gsize offset = 4 + 32 + 1; /* header(4) + hash(32) + body(1) */ + g_assert_cmpuint (FP_READ_UINT16_LE (flash + offset), ==, 4); /* block id */ + g_assert_cmpuint (FP_READ_UINT16_LE (flash + offset + 2), ==, + sizeof (priv_blob)); + + state.priv_blob = NULL; + state.server_cert = NULL; + state.ecdh_blob = NULL; + validity_pair_state_free (&state); +} + +/* ================================================================ + * T7.22: pair state init and free + * ================================================================ */ +static void +test_pair_state_lifecycle (void) +{ + ValidityPairState state; + + validity_pair_state_init (&state); + + g_assert_null (state.client_key); + g_assert_null (state.server_cert); + g_assert_null (state.ecdh_blob); + g_assert_null (state.priv_blob); + g_assert_cmpuint (state.num_partitions, ==, 0); + g_assert_cmpuint (state.erase_step, ==, 0); + + /* Free should be safe on empty state */ + validity_pair_state_free (&state); +} + +/* ================================================================ + * T7.23: pair state free with allocated resources + * ================================================================ */ +static void +test_pair_state_free_with_resources (void) +{ + ValidityPairState state; + + validity_pair_state_init (&state); + + state.server_cert = g_malloc (100); + state.server_cert_len = 100; + state.ecdh_blob = g_malloc (400); + state.ecdh_blob_len = 400; + state.priv_blob = g_malloc (161); + state.priv_blob_len = 161; + + /* Generate a key to test EVP_PKEY_free path */ + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id (EVP_PKEY_EC, NULL); + EVP_PKEY_keygen_init (pctx); + EVP_PKEY_CTX_set_ec_paramgen_curve_nid (pctx, NID_X9_62_prime256v1); + EVP_PKEY_keygen (pctx, &state.client_key); + EVP_PKEY_CTX_free (pctx); + g_assert_nonnull (state.client_key); + + /* Free should release all resources without leak */ + validity_pair_state_free (&state); +} + +/* ================================================================ + * T7.24: encrypt_key — different inputs produce different blobs + * ================================================================ */ +static void +test_encrypt_key_different_inputs (void) +{ + guint8 priv1[32], priv2[32], pub_x[32], pub_y[32]; + guint8 enc_key[32], val_key[32]; + + RAND_bytes (priv1, 32); + RAND_bytes (priv2, 32); + RAND_bytes (pub_x, 32); + RAND_bytes (pub_y, 32); + RAND_bytes (enc_key, 32); + RAND_bytes (val_key, 32); + + gsize len1, len2; + g_autofree guint8 *blob1 = validity_pair_encrypt_key (priv1, pub_x, pub_y, + enc_key, val_key, &len1); + g_autofree guint8 *blob2 = validity_pair_encrypt_key (priv2, pub_x, pub_y, + enc_key, val_key, &len2); + + g_assert_nonnull (blob1); + g_assert_nonnull (blob2); + g_assert_cmpuint (len1, ==, len2); + + /* Different private keys should produce different ciphertexts */ + g_assert_true (memcmp (blob1, blob2, len1) != 0); +} + +/* ================================================================ + * Tests: CAPTURE + * ================================================================ */ + + +/* ================================================================ + * T5.1: test_split_chunks_basic + * + * Verify that split_chunks correctly parses a TLV buffer with two + * known chunks and produces the right type, size, and data. + * ================================================================ */ +static void +test_split_chunks_basic (void) +{ + /* Build two TLV chunks: + * type=0x002a, size=4, data={0xAA,0xBB,0xCC,0xDD} + * type=0x0034, size=2, data={0x11,0x22} + */ + guint8 buf[] = { + 0x2a, 0x00, 0x04, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, + 0x34, 0x00, 0x02, 0x00, 0x11, 0x22, + }; + + gsize n = 0; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); + + g_assert_nonnull (chunks); + g_assert_cmpuint (n, ==, 2); + + g_assert_cmpuint (chunks[0].type, ==, 0x002a); + g_assert_cmpuint (chunks[0].size, ==, 4); + g_assert_cmpmem (chunks[0].data, 4, buf + 4, 4); + + g_assert_cmpuint (chunks[1].type, ==, 0x0034); + g_assert_cmpuint (chunks[1].size, ==, 2); + g_assert_cmpmem (chunks[1].data, 2, buf + 12, 2); + + validity_capture_chunks_free (chunks, n); +} + +/* ================================================================ + * T5.2: test_split_merge_roundtrip + * + * Verify that split then merge produces identical bytes. + * ================================================================ */ +static void +test_split_merge_roundtrip (void) +{ + guint8 buf[] = { + 0x2a, 0x00, 0x08, 0x00, + 0x20, 0x01, 0x01, 0x00, 0x10, 0x01, 0x00, 0x00, + 0x29, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, + }; + + gsize n = 0; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); + g_assert_nonnull (chunks); + g_assert_cmpuint (n, ==, 2); + + gsize merged_len = 0; + guint8 *merged = validity_capture_merge_chunks (chunks, n, &merged_len); + + g_assert_nonnull (merged); + g_assert_cmpuint (merged_len, ==, sizeof (buf)); + g_assert_cmpmem (merged, merged_len, buf, sizeof (buf)); + + g_free (merged); + validity_capture_chunks_free (chunks, n); +} + +/* ================================================================ + * T5.3: test_split_chunks_empty + * + * Verify empty input returns empty result. + * ================================================================ */ +static void +test_split_chunks_empty (void) +{ + gsize n = 99; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (NULL, 0, &n); + g_assert_null (chunks); + g_assert_cmpuint (n, ==, 0); +} + +/* ================================================================ + * T5.4: test_split_chunks_truncated + * + * Verify truncated chunk (size extends past end) returns NULL. + * ================================================================ */ +static void +test_split_chunks_truncated (void) +{ + /* type=0x0034, size=0x0008, but only 4 bytes of data follow */ + guint8 buf[] = { + 0x34, 0x00, 0x08, 0x00, 0x11, 0x22, 0x33, 0x44, + }; + + gsize n = 99; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); + g_assert_null (chunks); + g_assert_cmpuint (n, ==, 0); +} + +/* ================================================================ + * T5.5: test_decode_insn_noop + * + * Verify NOOP (0x00) decodes to opcode 0 with length 1. + * ================================================================ */ +static void +test_decode_insn_noop (void) +{ + guint8 data[] = { 0x00 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 1, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_NOOP); + g_assert_cmpuint (len, ==, 1); + g_assert_cmpuint (n_ops, ==, 0); +} + +/* ================================================================ + * T5.6: test_decode_insn_call + * + * Verify Call instruction (0x10-0x17) decodes correctly with + * rx_inc, address, and repeat operands. + * ================================================================ */ +static void +test_decode_insn_call (void) +{ + /* Call: rx_inc=2, address=0x0a*4=0x28, repeat=8 */ + guint8 data[] = { 0x12, 0x0a, 0x08 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_CALL); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (n_ops, ==, 3); + g_assert_cmpuint (operands[0], ==, 2); /* rx_inc */ + g_assert_cmpuint (operands[1], ==, 0x28); /* address = 0x0a << 2 */ + g_assert_cmpuint (operands[2], ==, 8); /* repeat */ +} + +/* ================================================================ + * T5.7: test_decode_insn_call_repeat_zero + * + * Verify Call with repeat byte 0x00 decodes to repeat=0x100. + * ================================================================ */ +static void +test_decode_insn_call_repeat_zero (void) +{ + guint8 data[] = { 0x10, 0x05, 0x00 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_CALL); + g_assert_cmpuint (operands[2], ==, 0x100); +} + +/* ================================================================ + * T5.8: test_decode_insn_regwrite + * + * Verify Register Write (0x40-0x7f) decodes correctly: + * register address = (b0 & 0x3f) * 4 + 0x80002000 + * value = u16 LE from bytes 1-2 + * ================================================================ */ +static void +test_decode_insn_regwrite (void) +{ + /* b0=0x4f → reg = (0x0f)*4 + 0x80002000 = 0x8000203C, value=0x1234 */ + guint8 data[] = { 0x4f, 0x34, 0x12 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_REG_WRITE); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (n_ops, ==, 2); + g_assert_cmpuint (operands[0], ==, 0x8000203c); + g_assert_cmpuint (operands[1], ==, 0x1234); +} + +/* ================================================================ + * T5.9: test_decode_insn_enable_rx + * + * Verify Enable Rx (opcode 6) decodes as 2-byte instruction. + * ================================================================ */ +static void +test_decode_insn_enable_rx (void) +{ + guint8 data[] = { 0x06, 0x42 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 2, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_ENABLE_RX); + g_assert_cmpuint (len, ==, 2); + g_assert_cmpuint (n_ops, ==, 1); + g_assert_cmpuint (operands[0], ==, 0x42); +} + +/* ================================================================ + * T5.10: test_decode_insn_sample + * + * Verify Sample (0x80-0xbf) decodes with two operands. + * ================================================================ */ +static void +test_decode_insn_sample (void) +{ + /* b0=0x8a → operand0 = (0x0a >> 3) & 7 = 1, operand1 = 0x0a & 7 = 2 */ + guint8 data[] = { 0x8a }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 1, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_SAMPLE); + g_assert_cmpuint (len, ==, 1); + g_assert_cmpuint (n_ops, ==, 2); + g_assert_cmpuint (operands[0], ==, 1); + g_assert_cmpuint (operands[1], ==, 2); +} + +/* ================================================================ + * T5.11: test_find_nth_insn + * + * Verify finding the Nth instruction of a given opcode in a buffer. + * ================================================================ */ +static void +test_find_nth_insn (void) +{ + /* Buffer: NOOP, NOOP, Call(rx=0,addr=0x14,rep=1), NOOP */ + guint8 data[] = { + 0x00, /* NOOP at offset 0 */ + 0x00, /* NOOP at offset 1 */ + 0x10, 0x05, 0x01, /* Call at offset 2 */ + 0x00, /* NOOP at offset 5 */ + }; + + /* 1st NOOP is at offset 0 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_NOOP, 1), ==, 0); + /* 2nd NOOP is at offset 1 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_NOOP, 2), ==, 1); + /* 3rd NOOP is at offset 5 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_NOOP, 3), ==, 5); + /* 1st Call is at offset 2 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_CALL, 1), ==, 2); + /* No 2nd Call */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_CALL, 2), ==, -1); +} + +/* ================================================================ + * T5.12: test_find_nth_regwrite + * + * Verify finding a Register Write to a specific register address. + * ================================================================ */ +static void +test_find_nth_regwrite (void) +{ + /* Buffer: RegWrite(0x80002000, 0x55), RegWrite(0x8000203C, 0xAB) */ + guint8 data[] = { + 0x40, 0x55, 0x00, /* reg = 0x80002000, val = 0x0055 */ + 0x4f, 0xAB, 0x00, /* reg = 0x8000203C, val = 0x00AB */ + }; + + /* Find 1st write to 0x8000203C → offset 3 */ + g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), + 0x8000203c, 1), ==, 3); + /* No 2nd write to 0x8000203C */ + g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), + 0x8000203c, 2), ==, -1); + /* Find 1st write to 0x80002000 → offset 0 */ + g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), + 0x80002000, 1), ==, 0); +} + +/* ================================================================ + * T5.13: test_patch_timeslot_table + * + * Verify that patch_timeslot_table multiplies Call repeat counts + * by the given multiplier. + * ================================================================ */ +static void +test_patch_timeslot_table (void) +{ + /* Call(rx=0, addr=0x14, repeat=3) followed by NOOP */ + guint8 data[] = { + 0x10, 0x05, 0x03, /* Call: repeat=3 */ + 0x00, /* NOOP */ + }; + + /* Multiply by 2, with inc_address=TRUE */ + g_assert_true (validity_capture_patch_timeslot_table (data, sizeof (data), + TRUE, 2)); + + /* repeat becomes 3*2=6 */ + g_assert_cmpuint (data[2], ==, 6); + /* address byte incremented */ + g_assert_cmpuint (data[1], ==, 6); +} + +/* ================================================================ + * T5.14: test_patch_timeslot_table_no_mult_for_repeat1 + * + * Verify that Call instructions with repeat <= 1 are NOT multiplied. + * ================================================================ */ +static void +test_patch_timeslot_table_no_mult_for_repeat1 (void) +{ + guint8 data[] = { + 0x10, 0x05, 0x01, /* Call: repeat=1 */ + 0x00, + }; + + g_assert_true (validity_capture_patch_timeslot_table (data, sizeof (data), + TRUE, 4)); + /* repeat stays 1 (not multiplied because <= 1) */ + g_assert_cmpuint (data[2], ==, 1); + /* address NOT incremented */ + g_assert_cmpuint (data[1], ==, 5); +} + +/* ================================================================ + * T5.15: test_bitpack_uniform + * + * When all values are identical, bitpack returns v0=0 (0 bits), + * v1=the common value, and zero-length packed data. + * ================================================================ */ +static void +test_bitpack_uniform (void) +{ + guint8 values[] = { 0x42, 0x42, 0x42, 0x42 }; + guint8 v0, v1; + gsize out_len; + + guint8 *packed = validity_capture_bitpack (values, 4, &v0, &v1, &out_len); + + g_assert_nonnull (packed); + g_assert_cmpuint (v0, ==, 0); + g_assert_cmpuint (v1, ==, 0x42); + g_assert_cmpuint (out_len, ==, 0); + + g_free (packed); +} + +/* ================================================================ + * T5.16: test_bitpack_range + * + * Verify bitpack with a small range of values. + * Values [10, 11, 12, 13] → delta range=3, useful_bits=2. + * ================================================================ */ +static void +test_bitpack_range (void) +{ + guint8 values[] = { 10, 11, 12, 13 }; + guint8 v0, v1; + gsize out_len; + + guint8 *packed = validity_capture_bitpack (values, 4, &v0, &v1, &out_len); + + g_assert_nonnull (packed); + g_assert_cmpuint (v0, ==, 2); /* 2 bits needed for max delta 3 */ + g_assert_cmpuint (v1, ==, 10); /* minimum value */ + + /* 4 values * 2 bits = 8 bits = 1 byte */ + g_assert_cmpuint (out_len, ==, 1); + + /* Deltas: [0, 1, 2, 3] + * Packed little-endian: bits 0-1 = 0b00, bits 2-3 = 0b01, + * bits 4-5 = 0b10, bits 6-7 = 0b11 + * Byte = 0b11100100 = 0xE4 */ + g_assert_cmpuint (packed[0], ==, 0xE4); + + g_free (packed); +} + +/* ================================================================ + * T5.17: test_factory_bits_parsing + * + * Verify parsing a synthetic factory bits response with subtag 3 + * (calibration values) and subtag 7 (calibration data). + * ================================================================ */ +static void +test_factory_bits_parsing (void) +{ + /* Factory bits response format: + * wtf(4LE) entries(4LE) + * entry: ptr(4LE) length(2LE) tag(2LE) subtag(2LE) flags(2LE) data[length] + */ + guint8 cal_values[] = { 0xAA, 0xBB, 0xCC, 0xDD }; + guint8 cal_data[] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }; + + /* Build response buffer */ + GByteArray *resp = g_byte_array_new (); + guint8 hdr[8]; + + /* Header: wtf=0, entries=2 */ + FP_WRITE_UINT32_LE (hdr, 0); + FP_WRITE_UINT32_LE (hdr + 4, 2); + g_byte_array_append (resp, hdr, 8); + + /* Entry 1: subtag=3, calibration values (4-byte header + actual data) */ + { + guint8 entry[12]; + guint16 length = 4 + sizeof (cal_values); /* 4-byte header + data */ + FP_WRITE_UINT32_LE (entry, 0); /* ptr */ + FP_WRITE_UINT16_LE (entry + 4, length); /* length */ + FP_WRITE_UINT16_LE (entry + 6, 0x0001); /* tag */ + FP_WRITE_UINT16_LE (entry + 8, 3); /* subtag = 3 */ + FP_WRITE_UINT16_LE (entry + 10, 0); /* flags */ + g_byte_array_append (resp, entry, 12); + + guint8 data_hdr[4] = { 0, 0, 0, 0 }; /* 4-byte header */ + g_byte_array_append (resp, data_hdr, 4); + g_byte_array_append (resp, cal_values, sizeof (cal_values)); + } + + /* Entry 2: subtag=7, calibration data (4-byte header + actual data) */ + { + guint8 entry[12]; + guint16 length = 4 + sizeof (cal_data); + FP_WRITE_UINT32_LE (entry, 0); + FP_WRITE_UINT16_LE (entry + 4, length); + FP_WRITE_UINT16_LE (entry + 6, 0x0002); + FP_WRITE_UINT16_LE (entry + 8, 7); /* subtag = 7 */ + FP_WRITE_UINT16_LE (entry + 10, 0); + g_byte_array_append (resp, entry, 12); + + guint8 data_hdr[4] = { 0, 0, 0, 0 }; + g_byte_array_append (resp, data_hdr, 4); + g_byte_array_append (resp, cal_data, sizeof (cal_data)); + } + + guint8 *out_cal_values = NULL, *out_cal_data = NULL; + gsize out_cal_values_len = 0, out_cal_data_len = 0; + + gboolean ok = validity_capture_parse_factory_bits ( + resp->data, resp->len, + &out_cal_values, &out_cal_values_len, + &out_cal_data, &out_cal_data_len); + + g_assert_true (ok); + g_assert_nonnull (out_cal_values); + g_assert_cmpuint (out_cal_values_len, ==, sizeof (cal_values)); + g_assert_cmpmem (out_cal_values, out_cal_values_len, + cal_values, sizeof (cal_values)); + + g_assert_nonnull (out_cal_data); + g_assert_cmpuint (out_cal_data_len, ==, sizeof (cal_data)); + g_assert_cmpmem (out_cal_data, out_cal_data_len, + cal_data, sizeof (cal_data)); + + g_free (out_cal_values); + g_free (out_cal_data); + g_byte_array_free (resp, TRUE); +} + +/* ================================================================ + * T5.18: test_factory_bits_no_subtag3 + * + * Verify that parsing fails when subtag 3 is missing. + * ================================================================ */ +static void +test_factory_bits_no_subtag3 (void) +{ + /* Build response with only subtag=7 (no subtag=3) */ + guint8 buf[32]; + + FP_WRITE_UINT32_LE (buf, 0); /* wtf */ + FP_WRITE_UINT32_LE (buf + 4, 1); /* entries=1 */ + + /* Entry: subtag=7, length=5 (4 hdr + 1 data) */ + FP_WRITE_UINT32_LE (buf + 8, 0); + FP_WRITE_UINT16_LE (buf + 12, 5); + FP_WRITE_UINT16_LE (buf + 14, 0x0001); + FP_WRITE_UINT16_LE (buf + 16, 7); /* subtag=7, not 3 */ + FP_WRITE_UINT16_LE (buf + 18, 0); + memset (buf + 20, 0, 5); + + guint8 *cv = NULL; + gsize cv_len = 0; + + gboolean ok = validity_capture_parse_factory_bits (buf, 25, + &cv, &cv_len, + NULL, NULL); + g_assert_false (ok); + g_assert_null (cv); +} + +/* ================================================================ + * T5.19: test_average_frames_interleave2 + * + * Verify frame averaging with interleave_lines=2 (repeat_multiplier=2). + * With 2 interleaved lines per calibration line, each output line + * should be the average of 2 input lines. + * ================================================================ */ +static void +test_average_frames_interleave2 (void) +{ + guint16 bytes_per_line = 4; + guint16 lines_per_calibration_data = 2; + guint16 lines_per_frame = 4; /* 2 cal lines * 2 interleave */ + guint8 calibration_frames = 1; + + /* Single frame: 4 lines * 4 bytes = 16 bytes */ + guint8 raw[] = { + 10, 20, 30, 40, /* line 0 (cal line 0, interleave 0) */ + 20, 30, 40, 50, /* line 1 (cal line 0, interleave 1) */ + 30, 40, 50, 60, /* line 2 (cal line 1, interleave 0) */ + 40, 50, 60, 70, /* line 3 (cal line 1, interleave 1) */ + }; + + gsize out_len = 0; + guint8 *result = validity_capture_average_frames ( + raw, sizeof (raw), + lines_per_frame, bytes_per_line, + lines_per_calibration_data, calibration_frames, + &out_len); + + g_assert_nonnull (result); + /* Output: 2 cal lines * 4 bytes = 8 bytes */ + g_assert_cmpuint (out_len, ==, 8); + + /* Cal line 0: avg of lines 0+1 → (10+20)/2=15, (20+30)/2=25, etc. */ + g_assert_cmpuint (result[0], ==, 15); + g_assert_cmpuint (result[1], ==, 25); + g_assert_cmpuint (result[2], ==, 35); + g_assert_cmpuint (result[3], ==, 45); + + /* Cal line 1: avg of lines 2+3 → (30+40)/2=35, (40+50)/2=45, etc. */ + g_assert_cmpuint (result[4], ==, 35); + g_assert_cmpuint (result[5], ==, 45); + g_assert_cmpuint (result[6], ==, 55); + g_assert_cmpuint (result[7], ==, 65); + + g_free (result); +} + +/* ================================================================ + * T5.20: test_clean_slate_roundtrip + * + * Verify that building a clean slate and then verifying it succeeds. + * ================================================================ */ +static void +test_clean_slate_roundtrip (void) +{ + guint8 test_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + gsize slate_len = 0; + + guint8 *slate = validity_capture_build_clean_slate (test_data, + sizeof (test_data), + &slate_len); + + g_assert_nonnull (slate); + g_assert_cmpuint (slate_len, >, 68); + + /* Magic should be 0x5002 */ + g_assert_cmpuint (FP_READ_UINT16_LE (slate), ==, 0x5002); + + /* Verify should pass */ + g_assert_true (validity_capture_verify_clean_slate (slate, slate_len)); + + /* Corrupt one byte and verify should fail */ + slate[70] ^= 0xff; + g_assert_false (validity_capture_verify_clean_slate (slate, slate_len)); + + g_free (slate); +} + +/* ================================================================ + * T5.21: test_finger_mapping + * + * Verify all 10 finger mappings work in both directions. + * ================================================================ */ +static void +test_finger_mapping (void) +{ + /* FpFinger enum: LEFT_THUMB=1, ..., RIGHT_LITTLE=10 */ + for (guint f = 1; f <= 10; f++) + { + guint16 subtype = validity_finger_to_subtype (f); + g_assert_cmpuint (subtype, ==, f); + + gint back = validity_subtype_to_finger (subtype); + g_assert_cmpint (back, ==, (gint) f); + } + + /* Out of range */ + g_assert_cmpuint (validity_finger_to_subtype (0), ==, 0); + g_assert_cmpuint (validity_finger_to_subtype (11), ==, 0); + g_assert_cmpint (validity_subtype_to_finger (0), ==, -1); + g_assert_cmpint (validity_subtype_to_finger (11), ==, -1); +} + +/* ================================================================ + * T5.22: test_led_commands + * + * Verify LED start/end commands have correct format. + * ================================================================ */ +static void +test_led_commands (void) +{ + gsize start_len = 0, end_len = 0; + const guint8 *start_cmd = validity_capture_glow_start_cmd (&start_len); + const guint8 *end_cmd = validity_capture_glow_end_cmd (&end_len); + + g_assert_nonnull (start_cmd); + g_assert_nonnull (end_cmd); + + /* Both should be 125 bytes (LED control payload) */ + g_assert_cmpuint (start_len, ==, 125); + g_assert_cmpuint (end_len, ==, 125); + + /* Both should start with cmd byte 0x39 */ + g_assert_cmpuint (start_cmd[0], ==, 0x39); + g_assert_cmpuint (end_cmd[0], ==, 0x39); +} + +/* ================================================================ + * T5.23: test_capture_prog_lookup + * + * Verify that CaptureProg lookup returns data for known devices + * and NULL for unknown ones. + * ================================================================ */ +static void +test_capture_prog_lookup (void) +{ + gsize len = 0; + + /* Known: firmware 6.x, dev_type 0xb5 */ + const guint8 *prog = validity_capture_prog_lookup (6, 7, 0x00b5, &len); + + g_assert_nonnull (prog); + g_assert_cmpuint (len, >, 0); + + /* The program should be parseable as TLV chunks */ + gsize n_chunks = 0; + ValidityCaptureChunk *chunks = validity_capture_split_chunks (prog, len, &n_chunks); + g_assert_nonnull (chunks); + g_assert_cmpuint (n_chunks, >=, 4); /* At least ACM, CEM, TST, offset */ + + /* Check that we have the expected chunk types */ + gboolean has_acm = FALSE, has_tst = FALSE, has_2d = FALSE; + for (gsize i = 0; i < n_chunks; i++) + { + if (chunks[i].type == 0x002a) + has_acm = TRUE; + if (chunks[i].type == CAPT_CHUNK_TIMESLOT_2D) + has_tst = TRUE; + if (chunks[i].type == CAPT_CHUNK_2D_PARAMS) + has_2d = TRUE; + } + g_assert_true (has_acm); + g_assert_true (has_tst); + g_assert_true (has_2d); + + validity_capture_chunks_free (chunks, n_chunks); + + /* Also check 0x0885 (same geometry) */ + prog = validity_capture_prog_lookup (6, 0, 0x0885, &len); + g_assert_nonnull (prog); + + /* Unknown: firmware 5.x */ + prog = validity_capture_prog_lookup (5, 0, 0x00b5, &len); + g_assert_null (prog); + + /* Unknown: dev_type not in type1 list */ + prog = validity_capture_prog_lookup (6, 0, 0x1234, &len); + g_assert_null (prog); +} + +/* ================================================================ + * T5.24: test_capture_state_setup + * + * Verify that state setup correctly initializes all fields from + * sensor type info and factory bits. + * ================================================================ */ +static void +test_capture_state_setup (void) +{ + ValidityCaptureState state; + const ValiditySensorTypeInfo *type_info; + + type_info = validity_sensor_type_info_lookup (0x00b5); + g_assert_nonnull (type_info); + + /* Build minimal factory bits response with subtag 3 */ + guint8 cal_vals[] = { 0x10, 0x20, 0x30 }; + GByteArray *fb = g_byte_array_new (); + guint8 hdr[8]; + FP_WRITE_UINT32_LE (hdr, 0); + FP_WRITE_UINT32_LE (hdr + 4, 1); + g_byte_array_append (fb, hdr, 8); + + guint8 entry[12]; + guint16 length = 4 + sizeof (cal_vals); + FP_WRITE_UINT32_LE (entry, 0); + FP_WRITE_UINT16_LE (entry + 4, length); + FP_WRITE_UINT16_LE (entry + 6, 1); + FP_WRITE_UINT16_LE (entry + 8, 3); + FP_WRITE_UINT16_LE (entry + 10, 0); + g_byte_array_append (fb, entry, 12); + + guint8 data_hdr[4] = { 0 }; + g_byte_array_append (fb, data_hdr, 4); + g_byte_array_append (fb, cal_vals, sizeof (cal_vals)); + + validity_capture_state_init (&state); + gboolean ok = validity_capture_state_setup (&state, type_info, + 0x00b5, 6, 7, + fb->data, fb->len); + + g_assert_true (ok); + g_assert_true (state.is_type1_device); + g_assert_cmpuint (state.bytes_per_line, ==, 0x78); + g_assert_cmpuint (state.lines_per_frame, ==, 112 * 2); /* 224 */ + g_assert_cmpuint (state.key_calibration_line, ==, 56); /* 112/2 */ + g_assert_cmpuint (state.calibration_frames, ==, 3); + g_assert_cmpuint (state.calibration_iterations, ==, 3); + + g_assert_nonnull (state.factory_calibration_values); + g_assert_cmpuint (state.factory_calibration_values_len, ==, sizeof (cal_vals)); + g_assert_cmpmem (state.factory_calibration_values, + state.factory_calibration_values_len, + cal_vals, sizeof (cal_vals)); + + g_assert_nonnull (state.capture_prog); + g_assert_cmpuint (state.capture_prog_len, >, 0); + + validity_capture_state_clear (&state); + g_byte_array_free (fb, TRUE); +} + +/* ================================================================ + * T5.25: test_build_cmd_02_header + * + * Verify that build_cmd_02 produces the expected 5-byte header: + * cmd(0x02) | bytes_per_line(2LE) | req_lines(2LE) | chunks... + * ================================================================ */ +static void +test_build_cmd_02_header (void) +{ + ValidityCaptureState state; + const ValiditySensorTypeInfo *type_info; + + type_info = validity_sensor_type_info_lookup (0x00b5); + g_assert_nonnull (type_info); + + validity_capture_state_init (&state); + + /* Minimal setup: just enough for build_cmd_02 */ + gsize prog_len; + state.capture_prog = validity_capture_prog_lookup (6, 7, 0x00b5, &prog_len); + g_assert_nonnull (state.capture_prog); + state.capture_prog_len = prog_len; + state.is_type1_device = TRUE; + state.bytes_per_line = type_info->bytes_per_line; + state.lines_per_frame = 224; + state.calibration_frames = 3; + state.key_calibration_line = 56; + + /* Need factory calibration values (even if empty) for line_update */ + state.factory_calibration_values = g_malloc0 (112); + state.factory_calibration_values_len = 112; + + gsize cmd_len = 0; + guint8 *cmd = validity_capture_build_cmd_02 (&state, type_info, + VALIDITY_CAPTURE_CALIBRATE, + &cmd_len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (cmd_len, >=, 5); + + /* Byte 0: command = 0x02 */ + g_assert_cmpuint (cmd[0], ==, 0x02); + + /* Bytes 1-2: bytes_per_line = 0x0078 */ + g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 1), ==, 0x0078); + + /* Bytes 3-4: req_lines for CALIBRATE = frames * lines_per_frame + 1 */ + guint16 expected_lines = 3 * 224 + 1; + g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 3), ==, expected_lines); + + /* Remainder should be parseable as TLV chunks */ + gsize n_chunks = 0; + ValidityCaptureChunk *chunks = validity_capture_split_chunks ( + cmd + 5, cmd_len - 5, &n_chunks); + g_assert_nonnull (chunks); + g_assert_cmpuint (n_chunks, >=, 4); + + validity_capture_chunks_free (chunks, n_chunks); + g_free (cmd); + + /* Test IDENTIFY mode: req_lines should be 0 */ + cmd = validity_capture_build_cmd_02 (&state, type_info, + VALIDITY_CAPTURE_IDENTIFY, + &cmd_len); + g_assert_nonnull (cmd); + g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 3), ==, 0); + g_free (cmd); + + g_free (state.factory_calibration_values); +} + +/* ================================================================ + * T5.26: test_calibration_processing + * + * Verify that process_calibration applies scale and accumulates. + * ================================================================ */ +static void +test_calibration_processing (void) +{ + guint16 bytes_per_line = 16; + /* Single line with 8-byte header + 8 bytes of data */ + guint8 frame[16] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* header (untouched) */ + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, /* data to scale */ + }; + + guint8 *calib = NULL; + gsize calib_len = 0; + + /* First call: initializes calib_data */ + validity_capture_process_calibration (&calib, &calib_len, + frame, sizeof (frame), + bytes_per_line); + + g_assert_nonnull (calib); + g_assert_cmpuint (calib_len, ==, 16); + + /* Header bytes should be preserved */ + g_assert_cmpuint (calib[0], ==, 0x00); + g_assert_cmpuint (calib[7], ==, 0x07); + + /* Data bytes at 0x80: scale(0x80) = (0x80 - 0x80) * 10 / 0x22 = 0 + * So all data bytes should be 0x00 */ + for (int i = 8; i < 16; i++) + g_assert_cmpuint (calib[i], ==, 0x00); + + /* Second call with same frame: accumulate */ + validity_capture_process_calibration (&calib, &calib_len, + frame, sizeof (frame), + bytes_per_line); + + /* add(0, 0) = 0, so data bytes still 0 */ + for (int i = 8; i < 16; i++) + g_assert_cmpuint (calib[i], ==, 0x00); + + g_free (calib); +} + +/* ================================================================ + * T5.27: test_capture_split_real_prog + * + * Parse the actual capture program for 0xb5 and verify + * expected chunks are present. + * ================================================================ */ +static void +test_capture_split_real_prog (void) +{ + gsize prog_len = 0; + const guint8 *prog = validity_capture_prog_lookup (6, 7, 0x00b5, &prog_len); + + g_assert_nonnull (prog); + + gsize n = 0; + ValidityCaptureChunk *chunks = validity_capture_split_chunks (prog, prog_len, &n); + + g_assert_nonnull (chunks); + g_assert_cmpuint (n, ==, 6); + + /* Expected order: 0x2a, 0x2c, 0x34, 0x2f, 0x29, 0x35 */ + g_assert_cmpuint (chunks[0].type, ==, 0x002a); + g_assert_cmpuint (chunks[0].size, ==, 8); + + g_assert_cmpuint (chunks[1].type, ==, 0x002c); + g_assert_cmpuint (chunks[1].size, ==, 40); + + g_assert_cmpuint (chunks[2].type, ==, CAPT_CHUNK_TIMESLOT_2D); + g_assert_cmpuint (chunks[2].size, ==, 64); + + g_assert_cmpuint (chunks[3].type, ==, CAPT_CHUNK_2D_PARAMS); + g_assert_cmpuint (chunks[3].size, ==, 4); + /* 2D value should be 112 (0x70) */ + g_assert_cmpuint (FP_READ_UINT32_LE (chunks[3].data), ==, 112); + + g_assert_cmpuint (chunks[4].type, ==, 0x0029); + g_assert_cmpuint (chunks[4].size, ==, 4); + + g_assert_cmpuint (chunks[5].type, ==, 0x0035); + g_assert_cmpuint (chunks[5].size, ==, 4); + + validity_capture_chunks_free (chunks, n); +} + +/* ================================================================ + * main + * ================================================================ */ + +/* ================================================================ + * Main + * ================================================================ */ + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* HAL tests */ + + g_test_add_func ("/validity/hal/lookup-all-types", + test_hal_lookup_all_types); + g_test_add_func ("/validity/hal/lookup-by-pid", + test_hal_lookup_by_pid); + g_test_add_func ("/validity/hal/lookup-invalid", + test_hal_lookup_invalid); + g_test_add_func ("/validity/hal/lookup-by-pid-invalid", + test_hal_lookup_by_pid_invalid); + g_test_add_func ("/validity/hal/blobs-present", + test_hal_blobs_present); + g_test_add_func ("/validity/hal/pid-0090-specifics", + test_hal_pid_0090_specifics); + g_test_add_func ("/validity/hal/clean-slate-present", + test_hal_clean_slate_present); + g_test_add_func ("/validity/hal/flash-layout", + test_hal_flash_layout); + g_test_add_func ("/validity/hal/blob-sizes", + test_hal_blob_sizes); + g_test_add_func ("/validity/hal/lookup-consistency", + test_hal_lookup_consistency); + + + /* SENSOR tests */ + + g_test_add_func ("/validity/sensor/identify/parse", + test_identify_sensor_parse); + g_test_add_func ("/validity/sensor/identify/truncated", + test_identify_sensor_parse_truncated); + g_test_add_func ("/validity/sensor/devinfo/lookup_exact", + test_device_info_lookup_exact); + g_test_add_func ("/validity/sensor/devinfo/lookup_another", + test_device_info_lookup_another); + g_test_add_func ("/validity/sensor/devinfo/lookup_unknown", + test_device_info_lookup_unknown); + g_test_add_func ("/validity/sensor/devinfo/lookup_fuzzy", + test_device_info_lookup_fuzzy); + g_test_add_func ("/validity/sensor/typeinfo/lookup", + test_sensor_type_info_lookup); + g_test_add_func ("/validity/sensor/typeinfo/lookup_db", + test_sensor_type_info_lookup_db); + g_test_add_func ("/validity/sensor/typeinfo/lookup_unknown", + test_sensor_type_info_lookup_unknown); + g_test_add_func ("/validity/sensor/factory_bits/cmd_format", + test_factory_bits_cmd_format); + g_test_add_func ("/validity/sensor/factory_bits/buffer_too_small", + test_factory_bits_cmd_buffer_too_small); + g_test_add_func ("/validity/sensor/identify_then_lookup", + test_identify_then_lookup); + g_test_add_func ("/validity/sensor/state_lifecycle", + test_sensor_state_lifecycle); + g_test_add_func ("/validity/sensor/calibration_blob_present", + test_calibration_blob_present); + + + /* ENROLL tests */ + + g_test_add_func ("/validity/enroll/parse-empty", + test_parse_empty); + g_test_add_func ("/validity/enroll/parse-template-block", + test_parse_template_block); + g_test_add_func ("/validity/enroll/parse-header-block", + test_parse_header_block); + g_test_add_func ("/validity/enroll/parse-tid-block", + test_parse_tid_block); + g_test_add_func ("/validity/enroll/parse-multiple-blocks", + test_parse_multiple_blocks); + g_test_add_func ("/validity/enroll/parse-truncated", + test_parse_truncated); + g_test_add_func ("/validity/enroll/parse-unknown-tag", + test_parse_unknown_tag); + g_test_add_func ("/validity/enroll/result-clear", + test_result_clear); + g_test_add_func ("/validity/enroll/parse-zero-length-payload", + test_parse_zero_length_payload); + + + /* FWEXT tests */ + + /* Firmware info parsing */ + g_test_add_func ("/validity/fwext/fw-info/parse-present", + test_fw_info_parse_present); + g_test_add_func ("/validity/fwext/fw-info/parse-absent", + test_fw_info_parse_absent); + g_test_add_func ("/validity/fwext/fw-info/parse-unknown-status", + test_fw_info_parse_unknown_status); + g_test_add_func ("/validity/fwext/fw-info/parse-truncated", + test_fw_info_parse_truncated); + + /* File parsing */ + g_test_add_func ("/validity/fwext/file/parse", + test_xpfwext_file_parse); + g_test_add_func ("/validity/fwext/file/no-delimiter", + test_xpfwext_file_no_delimiter); + g_test_add_func ("/validity/fwext/file/too-short", + test_xpfwext_file_too_short); + g_test_add_func ("/validity/fwext/file/clear-idempotent", + test_file_clear_idempotent); + + /* Command format */ + g_test_add_func ("/validity/fwext/cmd/write-flash", + test_flash_write_cmd_format); + g_test_add_func ("/validity/fwext/cmd/write-fw-sig", + test_fw_sig_cmd_format); + g_test_add_func ("/validity/fwext/cmd/write-hw-reg", + test_hw_reg_write_cmd_format); + g_test_add_func ("/validity/fwext/cmd/read-hw-reg", + test_hw_reg_read_cmd_format); + g_test_add_func ("/validity/fwext/cmd/read-hw-reg-parse", + test_hw_reg_read_parse); + g_test_add_func ("/validity/fwext/cmd/reboot", + test_reboot_cmd_format); + + /* Chunk iteration */ + g_test_add_func ("/validity/fwext/chunk-iteration", + test_chunk_iteration); + + /* Firmware filename mapping */ + g_test_add_func ("/validity/fwext/firmware-name", + test_firmware_filename); + g_test_add_func ("/validity/fwext/find-firmware/missing", + test_missing_firmware_file); + g_test_add_func ("/validity/fwext/find-firmware/unsupported-pid", + test_unsupported_pid_firmware); + + /* Blob lookup */ + g_test_add_func ("/validity/fwext/db-write-enable", + test_fwext_db_write_enable_blob); + + + /* DB tests */ + + /* Command builder tests */ + g_test_add_func ("/validity/db/cmd_db_info", test_cmd_db_info); + g_test_add_func ("/validity/db/cmd_get_user_storage", test_cmd_get_user_storage); + g_test_add_func ("/validity/db/cmd_get_user_storage_null_name", test_cmd_get_user_storage_null_name); + g_test_add_func ("/validity/db/cmd_get_user", test_cmd_get_user); + g_test_add_func ("/validity/db/cmd_lookup_user", test_cmd_lookup_user); + g_test_add_func ("/validity/db/cmd_new_record", test_cmd_new_record); + g_test_add_func ("/validity/db/cmd_del_record", test_cmd_del_record); + g_test_add_func ("/validity/db/cmd_create_enrollment", test_cmd_create_enrollment); + g_test_add_func ("/validity/db/cmd_enrollment_update_start", test_cmd_enrollment_update_start); + g_test_add_func ("/validity/db/cmd_enrollment_update", test_cmd_enrollment_update); + g_test_add_func ("/validity/db/cmd_match_finger", test_cmd_match_finger); + g_test_add_func ("/validity/db/cmd_get_match_result", test_cmd_get_match_result); + g_test_add_func ("/validity/db/cmd_match_cleanup", test_cmd_match_cleanup); + g_test_add_func ("/validity/db/cmd_get_prg_status", test_cmd_get_prg_status); + g_test_add_func ("/validity/db/cmd_capture_stop", test_cmd_capture_stop); + g_test_add_func ("/validity/db/cmd_call_cleanups", test_cmd_call_cleanups); + g_test_add_func ("/validity/db/cmd_get_record_value", test_cmd_get_record_value); + g_test_add_func ("/validity/db/cmd_get_record_children", test_cmd_get_record_children); + + /* Response parser tests */ + g_test_add_func ("/validity/db/parse_info", test_parse_db_info); + g_test_add_func ("/validity/db/parse_info_too_short", test_parse_db_info_too_short); + g_test_add_func ("/validity/db/parse_user_storage", test_parse_user_storage); + g_test_add_func ("/validity/db/parse_user", test_parse_user); + g_test_add_func ("/validity/db/parse_new_record_id", test_parse_new_record_id); + g_test_add_func ("/validity/db/parse_new_record_id_too_short", test_parse_new_record_id_too_short); + g_test_add_func ("/validity/db/parse_record_value", test_parse_record_value); + g_test_add_func ("/validity/db/parse_record_children", test_parse_record_children); + + /* Identity and finger data tests */ + g_test_add_func ("/validity/db/build_identity", test_build_identity); + g_test_add_func ("/validity/db/build_finger_data", test_build_finger_data); + + /* Blob accessor test */ + g_test_add_func ("/validity/db/write_enable_blob", test_db_write_enable_blob); + + + /* VERIFY tests */ + + /* R1: parse_match_result regression tests (Issue #1: dead while loop) */ + g_test_add_func ("/validity/verify/parse_match_result_valid", + test_parse_match_result_valid); + g_test_add_func ("/validity/verify/parse_match_result_multi_tags", + test_parse_match_result_multi_tags); + g_test_add_func ("/validity/verify/parse_match_result_empty", + test_parse_match_result_empty); + g_test_add_func ("/validity/verify/parse_match_result_truncated", + test_parse_match_result_truncated); + g_test_add_func ("/validity/verify/parse_match_result_unknown_tags", + test_parse_match_result_unknown_tags); + g_test_add_func ("/validity/verify/match_result_clear", + test_match_result_clear); + + /* R2: identity builder NULL regression (Issue #2: NULL crash) */ + g_test_add_func ("/validity/verify/build_identity_null", + test_build_identity_null); + g_test_add_func ("/validity/verify/build_identity_valid_uuid", + test_build_identity_valid_uuid); + + /* R3: gallery matching by subtype (Issue #3: always returned first) */ + g_test_add_func ("/validity/verify/gallery_match_by_subtype", + test_gallery_match_by_subtype); + g_test_add_func ("/validity/verify/gallery_match_fallback", + test_gallery_match_fallback); + g_test_add_func ("/validity/verify/gallery_match_empty", + test_gallery_match_empty); + + /* R4: struct field separation (Issue #4: field abuse) */ + g_test_add_func ("/validity/verify/struct_separate_fields", + test_struct_separate_fields); + + /* R5: del_record command format (Issue #5: delete SSM non-functional) */ + g_test_add_func ("/validity/verify/del_record_format", + test_del_record_format); + + /* R6: match_finger single allocation (Issue #6: double alloc) */ + g_test_add_func ("/validity/verify/match_finger_size", + test_match_finger_size); + + /* R7: clear/delete storage SSM states (Issue #7: stub) */ + g_test_add_func ("/validity/verify/clear_storage_states", + test_clear_storage_states_exist); + g_test_add_func ("/validity/verify/delete_states", + test_delete_states_exist); + + + /* TLS tests */ + + g_test_add_func ("/validity/tls/prf/deterministic", test_prf_deterministic); + g_test_add_func ("/validity/tls/prf/output-length", test_prf_output_length); + g_test_add_func ("/validity/tls/prf/short-output", test_prf_short_output); + g_test_add_func ("/validity/tls/encrypt/roundtrip", + test_encrypt_decrypt_roundtrip); + g_test_add_func ("/validity/tls/encrypt/block-aligned", + test_encrypt_block_aligned); + g_test_add_func ("/validity/tls/decrypt/invalid", test_decrypt_invalid); + g_test_add_func ("/validity/tls/psk/derivation", test_psk_derivation); + g_test_add_func ("/validity/tls/psk/deterministic", test_psk_deterministic); + g_test_add_func ("/validity/tls/flash/parse-empty", test_flash_parse_empty); + g_test_add_func ("/validity/tls/flash/parse-truncated", + test_flash_parse_truncated); + g_test_add_func ("/validity/tls/init-free", test_init_free); + g_test_add_func ("/validity/tls/client-hello", test_build_client_hello); + g_test_add_func ("/validity/tls/unwrap/invalid", test_unwrap_invalid); + + /* Regression tests for hardware-discovered bugs */ + g_test_add_func ("/validity/tls/regression/flash-parse-needs-psk", + test_flash_parse_needs_psk); + g_test_add_func ("/validity/tls/regression/flash-cmd-format", + test_flash_cmd_format); + g_test_add_func ("/validity/tls/regression/flash-response-header", + test_flash_response_header); + g_test_add_func ("/validity/tls/regression/server-hello-rejects-vcsfw-prefix", + test_server_hello_rejects_vcsfw_prefix); + g_test_add_func ("/validity/tls/regression/client-hello-tls-prefix", + test_client_hello_tls_prefix); + + + /* PAIR tests */ + + g_test_add_func ("/validity/pair/parse-flash-info-valid", + test_parse_flash_info_valid); + g_test_add_func ("/validity/pair/parse-flash-info-needs-pairing", + test_parse_flash_info_needs_pairing); + g_test_add_func ("/validity/pair/parse-flash-info-too-short", + test_parse_flash_info_too_short); + g_test_add_func ("/validity/pair/serialize-partition", + test_serialize_partition); + g_test_add_func ("/validity/pair/make-cert-size", + test_make_cert_size); + g_test_add_func ("/validity/pair/make-cert-deterministic", + test_make_cert_deterministic); + g_test_add_func ("/validity/pair/encrypt-key-structure", + test_encrypt_key_structure); + g_test_add_func ("/validity/pair/encrypt-key-hmac-valid", + test_encrypt_key_hmac_valid); + g_test_add_func ("/validity/pair/build-partition-flash-cmd", + test_build_partition_flash_cmd); + g_test_add_func ("/validity/pair/build-tls-flash-size", + test_build_tls_flash_size); + g_test_add_func ("/validity/pair/build-tls-flash-blocks", + test_build_tls_flash_blocks); + g_test_add_func ("/validity/pair/state-lifecycle", + test_pair_state_lifecycle); + g_test_add_func ("/validity/pair/state-free-with-resources", + test_pair_state_free_with_resources); + g_test_add_func ("/validity/pair/encrypt-key-different-inputs", + test_encrypt_key_different_inputs); + + + /* CAPTURE tests */ + + /* Chunk parsing */ + g_test_add_func ("/validity/capture/split-chunks-basic", + test_split_chunks_basic); + g_test_add_func ("/validity/capture/split-merge-roundtrip", + test_split_merge_roundtrip); + g_test_add_func ("/validity/capture/split-chunks-empty", + test_split_chunks_empty); + g_test_add_func ("/validity/capture/split-chunks-truncated", + test_split_chunks_truncated); + + /* Timeslot instruction decoder */ + g_test_add_func ("/validity/capture/decode-insn-noop", + test_decode_insn_noop); + g_test_add_func ("/validity/capture/decode-insn-call", + test_decode_insn_call); + g_test_add_func ("/validity/capture/decode-insn-call-repeat-zero", + test_decode_insn_call_repeat_zero); + g_test_add_func ("/validity/capture/decode-insn-regwrite", + test_decode_insn_regwrite); + g_test_add_func ("/validity/capture/decode-insn-enable-rx", + test_decode_insn_enable_rx); + g_test_add_func ("/validity/capture/decode-insn-sample", + test_decode_insn_sample); + + /* Instruction search */ + g_test_add_func ("/validity/capture/find-nth-insn", + test_find_nth_insn); + g_test_add_func ("/validity/capture/find-nth-regwrite", + test_find_nth_regwrite); + + /* Timeslot patching */ + g_test_add_func ("/validity/capture/patch-timeslot-table", + test_patch_timeslot_table); + g_test_add_func ("/validity/capture/patch-timeslot-no-mult-repeat1", + test_patch_timeslot_table_no_mult_for_repeat1); + + /* Bitpack */ + g_test_add_func ("/validity/capture/bitpack-uniform", + test_bitpack_uniform); + g_test_add_func ("/validity/capture/bitpack-range", + test_bitpack_range); + + /* Factory bits */ + g_test_add_func ("/validity/capture/factory-bits-parsing", + test_factory_bits_parsing); + g_test_add_func ("/validity/capture/factory-bits-no-subtag3", + test_factory_bits_no_subtag3); + + /* Frame averaging */ + g_test_add_func ("/validity/capture/average-frames-interleave2", + test_average_frames_interleave2); + + /* Clean slate */ + g_test_add_func ("/validity/capture/clean-slate-roundtrip", + test_clean_slate_roundtrip); + + /* Finger mapping */ + g_test_add_func ("/validity/capture/finger-mapping", + test_finger_mapping); + + /* LED commands */ + g_test_add_func ("/validity/capture/led-commands", + test_led_commands); + + /* CaptureProg lookup */ + g_test_add_func ("/validity/capture/prog-lookup", + test_capture_prog_lookup); + + /* State setup */ + g_test_add_func ("/validity/capture/state-setup", + test_capture_state_setup); + + /* build_cmd_02 */ + g_test_add_func ("/validity/capture/build-cmd-02-header", + test_build_cmd_02_header); + + /* Calibration processing */ + g_test_add_func ("/validity/capture/calibration-processing", + test_calibration_processing); + + /* Real capture program parsing */ + g_test_add_func ("/validity/capture/split-real-prog", + test_capture_split_real_prog); + + + return g_test_run (); +}