/* * 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 (); }