libfprint/tests/test-validity.c

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

5080 lines
173 KiB
C
Raw Normal View History

/*
* 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 <glib.h>
#include <string.h>
#include <glib/gstdio.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/core_names.h>
#include <openssl/param_build.h>
#include <openssl/ec.h>
#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('<BBBHLL', 0x40, partition, 1, 0, addr, size).
* Old code only sent 10 bytes, missing the access flag and reserved
* field. This test verifies the command constant and expected layout.
* ================================================================ */
static void
test_flash_cmd_format (void)
{
/* Verify the command byte */
g_assert_cmpint (VCSFW_CMD_READ_FLASH, ==, 0x40);
/* Build the same command as validity_tls_flash_read_run_state does */
guint8 cmd[13];
cmd[0] = VCSFW_CMD_READ_FLASH;
cmd[1] = 0x01; /* partition */
cmd[2] = 0x01; /* access flag */
FP_WRITE_UINT16_LE (&cmd[3], 0x0000); /* reserved */
FP_WRITE_UINT32_LE (&cmd[5], 0x0000); /* offset */
FP_WRITE_UINT32_LE (&cmd[9], 0x1000); /* size */
/* Verify total size is 13 (not 10 like the old bug) */
g_assert_cmpuint (sizeof (cmd), ==, 13);
/* Verify byte layout matches python-validity's pack('<BBBHLL', ...) */
g_assert_cmpint (cmd[0], ==, 0x40); /* command */
g_assert_cmpint (cmd[1], ==, 0x01); /* partition */
g_assert_cmpint (cmd[2], ==, 0x01); /* access flag (was missing) */
g_assert_cmpint (cmd[3], ==, 0x00); /* reserved lo */
g_assert_cmpint (cmd[4], ==, 0x00); /* reserved hi */
/* offset at bytes 5-8 (LE uint32 = 0) */
g_assert_cmpint (cmd[5], ==, 0x00);
g_assert_cmpint (cmd[6], ==, 0x00);
g_assert_cmpint (cmd[7], ==, 0x00);
g_assert_cmpint (cmd[8], ==, 0x00);
/* size at bytes 9-12 (LE uint32 = 0x1000) */
g_assert_cmpint (cmd[9], ==, 0x00);
g_assert_cmpint (cmd[10], ==, 0x10);
g_assert_cmpint (cmd[11], ==, 0x00);
g_assert_cmpint (cmd[12], ==, 0x00);
}
/* ================================================================
* Regression: Bug #3 Flash response has 6-byte header
*
* After vcsfw_cmd_send strips the 2-byte VCSFW status, the flash
* response still contains a 6-byte header: [size:4 LE][unknown:2].
* Actual flash data starts at offset 6. Old code passed the raw
* response directly to parse_flash(), corrupting the block parsing.
* This test verifies:
* 1) The 6-byte header is correctly structured (size field matches)
* 2) parse_flash works on correctly unwrapped data (offset +6)
* 3) The raw response differs from the unwrapped payload, proving
* that skipping the header is necessary
* ================================================================ */
static void
test_flash_response_header (void)
{
/* Build a minimal valid flash image: just an end marker */
guint8 flash_data[] = { 0xFF, 0xFF, 0x00, 0x00 };
/* Wrap it in the response header format: [size:4 LE][unk:2][data] */
guint32 data_size = sizeof (flash_data);
guint8 response[6 + sizeof (flash_data)];
FP_WRITE_UINT32_LE (response, data_size);
response[4] = 0x00; /* unknown byte 1 */
response[5] = 0x00; /* unknown byte 2 */
memcpy (response + 6, flash_data, sizeof (flash_data));
/* Verify the unwrap logic: read size from offset 0, skip 6-byte header */
guint32 read_size = FP_READ_UINT32_LE (response);
g_assert_cmpuint (read_size, ==, sizeof (flash_data));
const guint8 *payload = response + 6;
gsize payload_len = sizeof (response) - 6;
g_assert_cmpuint (payload_len, ==, sizeof (flash_data));
/* Parsing the correctly unwrapped data should succeed (end marker only = no keys) */
ValidityTlsState tls;
validity_tls_init (&tls);
GError *error = NULL;
gboolean result = validity_tls_parse_flash (&tls, payload, payload_len, &error);
/* Expected: fails because no keys, but NOT because of corrupt block headers */
g_assert_false (result);
g_assert_nonnull (error);
g_assert_true (g_str_has_prefix (error->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 ();
}