libfprint/tests/test-validity.c
Leonardo Francisco 78fcfd3cd4 tests: revise comments — remove debug-finding language and issue tracking
Drop 'Regression: Issue #N' / 'Bug #N' numbering, 'dead while loop',
'the old code', 'This catches the bug where' and similar changelog-style
language from test comments. Keep concise descriptions of what each test
validates. Also simplify data-loader test headers and the verify
interrupt comment.
2026-04-22 03:06:34 +00:00

5722 lines
193 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <unistd.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 flash layout (blobs loaded at runtime)
* ================================================================ */
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);
/* All devices must have vid, pid, and flash_layout */
g_assert_cmpuint (desc->vid, !=, 0);
g_assert_cmpuint (desc->pid, !=, 0);
g_assert_nonnull (desc->flash_layout);
}
}
/* ================================================================
* T7.6: PID 0090 flash layout
* ================================================================ */
static void
test_hal_pid_0090_specifics (void)
{
const ValidityDeviceDesc *desc = validity_hal_device_lookup (VALIDITY_DEV_90);
g_assert_nonnull (desc);
/* 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 also have flash layout
* ================================================================ */
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->flash_layout);
}
}
/* ================================================================
* 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);
/* 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: Device descriptors have correct VID/PID
* ================================================================ */
static void
test_hal_blob_sizes (void)
{
const ValidityDeviceDesc *desc_9a =
validity_hal_device_lookup (VALIDITY_DEV_9A);
g_assert_nonnull (desc_9a);
g_assert_cmpuint (desc_9a->vid, ==, 0x06cb);
g_assert_cmpuint (desc_9a->pid, ==, 0x009a);
const ValidityDeviceDesc *desc_90 =
validity_hal_device_lookup (VALIDITY_DEV_90);
g_assert_nonnull (desc_90);
g_assert_cmpuint (desc_90->vid, ==, 0x138a);
g_assert_cmpuint (desc_90->pid, ==, 0x0090);
}
/* ================================================================
* 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
*
* Since blobs are now loaded from external data files at runtime,
* this test verifies the function returns NULL when no data is loaded.
* ================================================================ */
static void
test_fwext_db_write_enable_blob (void)
{
/* Create a minimal FpiDeviceValidity with empty data stores.
* Since no data files are loaded, the accessor should return NULL. */
FpiDeviceValidity dev = { 0 };
validity_data_store_init (&dev.device_data);
gsize len;
const guint8 *blob = validity_fwext_get_db_write_enable (&dev, &len);
g_assert_null (blob);
validity_data_store_free (&dev.device_data);
}
/* ================================================================
* 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);
}
/* ================================================================
* 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
*
* Since blobs are now loaded from external data files at runtime,
* verify the function returns NULL when no data is loaded.
* ================================================================ */
static void
test_db_write_enable_blob (void)
{
FpiDeviceValidity dev = { 0 };
validity_data_store_init (&dev.device_data);
gsize len;
const guint8 *blob = validity_db_get_write_enable_blob (&dev, &len);
g_assert_null (blob);
validity_data_store_free (&dev.device_data);
}
/* ================================================================
* 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
*
* Verifies that user_dbid, subtype, and hash are correctly parsed
* from a TLV dictionary.
* ================================================================ */
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
*
* Tags appear in non-sequential order (tag 3 before tag 1) to
* verify the parser processes every entry, not just the first.
* ================================================================ */
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)
*
* 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 user_id
*
* A NULL user_id must return NULL instead of crashing in
* g_variant_new_string().
* ================================================================ */
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
*
* UUID → identity bytes round-trip (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
*
* Identify must match by finger subtype, not just return the
* first gallery entry.
* ================================================================ */
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 and delete_storage_dbid are separate fields
*
* Both fields must exist independently in the device struct.
* ================================================================ */
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
*
* Verify cmd 0x48 produces correct format: 0x48 | dbid(2LE).
* ================================================================ */
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
*
* build_cmd_match_finger must produce a single 13-byte buffer.
* ================================================================ */
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
*
* CLEAR_* enum states must be defined for clear_storage to work.
* ================================================================ */
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 const guint8 test_password[32] = {
0x97, 0x04, 0x4D, 0x1A, 0xF0, 0x66, 0xAD, 0x8D,
0x18, 0x1A, 0x6E, 0xE5, 0xC1, 0x55, 0x79, 0x31,
0x3B, 0xA5, 0x77, 0xCC, 0x59, 0xD2, 0x0B, 0x10,
0xD0, 0x4B, 0x8E, 0xC8, 0x9D, 0xBA, 0x4C, 0x86,
};
static const guint8 test_gwk_sign[32] = {
0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18,
0x29, 0x3A, 0x4B, 0x5C, 0x6D, 0x7E, 0x8F, 0x90,
0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78,
0x89, 0x9A, 0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0xF0,
};
static void
test_psk_derivation (void)
{
ValidityTlsState tls;
validity_tls_init (&tls);
tls.password = test_password;
tls.password_len = sizeof (test_password);
tls.gwk_sign = test_gwk_sign;
tls.gwk_sign_len = sizeof (test_gwk_sign);
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);
tls1.password = test_password;
tls1.password_len = sizeof (test_password);
tls1.gwk_sign = test_gwk_sign;
tls1.gwk_sign_len = sizeof (test_gwk_sign);
tls2.password = test_password;
tls2.password_len = sizeof (test_password);
tls2.gwk_sign = test_gwk_sign;
tls2.gwk_sign_len = sizeof (test_gwk_sign);
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);
}
/* ================================================================
* Flash parse requires PSK before private key decryption
*
* Private key block (ID 4) is encrypted with PSK. parse_flash
* without prior PSK derivation must fail (HMAC mismatch).
* ================================================================ */
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);
/* Set up key material before PSK derivation */
tls_with_psk.password = test_password;
tls_with_psk.password_len = sizeof (test_password);
tls_with_psk.gwk_sign = test_gwk_sign;
tls_with_psk.gwk_sign_len = sizeof (test_gwk_sign);
/* 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. */
/* Build a cert body */
guint8 cert_body[16];
memset (cert_body, 0xAA, sizeof (cert_body));
/* 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);
}
/* ================================================================
* READ_FLASH command format
*
* Must be exactly 13 bytes: pack('<BBBHLL', 0x40, partition, 1, 0,
* addr, size) — including the access flag and reserved field.
* ================================================================ */
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 */
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 */
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);
}
/* ================================================================
* Flash response has 6-byte header before block data
*
* After stripping the 2-byte VCSFW status, the flash response
* contains [size:4 LE][unknown:2] before the actual block data.
* parse_flash must receive data starting at offset 6.
* ================================================================ */
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);
/* The raw response (with 6-byte header) differs from the unwrapped
* payload — passing it directly to parse_flash would corrupt block
* parsing since the size field (0x04 0x00 ...) looks like block_id=4. */
g_assert_cmpuint (sizeof (response), !=, payload_len);
g_assert_true (memcmp (response, payload, payload_len) != 0);
}
/* ================================================================
* TLS handshake uses raw TLS records (not VCSFW-wrapped)
*
* parse_server_hello expects data starting with content type 0x16.
* A 2-byte VCSFW status prefix would corrupt the TLS record.
* ================================================================ */
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);
}
/* ================================================================
* Client hello uses 0x44 prefix (not a VCSFW command)
*
* TLS handshake messages are prefixed with 0x44000000, followed
* by the raw TLS record. No VCSFW status in the 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;
guint8 password[32];
RAND_bytes (password, sizeof (password));
g_autofree guint8 *cert = validity_pair_make_cert (pub_x, pub_y,
password, sizeof (password),
&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 };
guint8 password[32] = { 0x03 };
gsize len1, len2;
g_autofree guint8 *cert1 = validity_pair_make_cert (pub_x, pub_y,
password, sizeof (password),
&len1);
g_autofree guint8 *cert2 = validity_pair_make_cert (pub_x, pub_y,
password, sizeof (password),
&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);
/* Create dummy data for runtime parameters */
guint8 partition_sig[256];
guint8 password[32];
guint8 ca_cert[256];
RAND_bytes (partition_sig, sizeof (partition_sig));
RAND_bytes (password, sizeof (password));
RAND_bytes (ca_cert, sizeof (ca_cert));
gsize cmd_len;
g_autofree guint8 *cmd =
validity_pair_build_partition_flash_cmd (&flash_ic, desc->flash_layout,
partition_sig, sizeof (partition_sig),
pub_x, pub_y,
password, sizeof (password),
ca_cert, sizeof (ca_cert),
&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);
guint8 ca_cert[256];
RAND_bytes (ca_cert, sizeof (ca_cert));
gsize flash_len;
g_autofree guint8 *flash = validity_pair_build_tls_flash (&state,
ca_cert, sizeof (ca_cert),
&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);
guint8 ca_cert2[256];
memset (ca_cert2, 0xDD, sizeof (ca_cert2));
gsize flash_len;
g_autofree guint8 *flash = validity_pair_build_tls_flash (&state,
ca_cert2, sizeof (ca_cert2),
&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);
}
/* ================================================================
* Tests: Data Loader (validity_data.c)
* ================================================================ */
/* ================================================================
* T8.1: Store init/free — empty store returns NULL for all tags
* ================================================================ */
static void
test_data_store_empty (void)
{
ValidityDataStore store;
validity_data_store_init (&store);
/* All tags should return NULL */
for (int tag = 0; tag < VALIDITY_DATA_NUM_TAGS; tag++)
{
gsize len;
const guint8 *data = validity_data_get_bytes (&store, tag, &len);
g_assert_null (data);
g_assert_cmpuint (len, ==, 0);
GBytes *bytes = validity_data_get (&store, tag);
g_assert_null (bytes);
}
validity_data_store_free (&store);
}
/* ================================================================
* T8.2: Store double free — should not crash
* ================================================================ */
static void
test_data_store_double_free (void)
{
ValidityDataStore store;
validity_data_store_init (&store);
validity_data_store_free (&store);
validity_data_store_free (&store);
}
/* ================================================================
* T8.3: Load file with valid HMAC
* ================================================================ */
static void
test_data_load_valid_hmac (void)
{
/* Create a temp file with valid HMAC-SHA256 */
guint8 payload[] = { 0xDE, 0xAD, 0xBE, 0xEF };
gsize payload_len = sizeof (payload);
/* Compute HMAC-SHA256 with the known key */
const guint8 hmac_key[32] = {
0x6c, 0x69, 0x62, 0x66, 0x70, 0x72, 0x69, 0x6e,
0x74, 0x2d, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69,
0x74, 0x79, 0x2d, 0x64, 0x61, 0x74, 0x61, 0x2d,
0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74,
};
GHmac *hmac = g_hmac_new (G_CHECKSUM_SHA256, hmac_key, sizeof (hmac_key));
g_hmac_update (hmac, payload, payload_len);
guint8 trailer[32];
gsize digest_len = sizeof (trailer);
g_hmac_get_digest (hmac, trailer, &digest_len);
g_hmac_unref (hmac);
/* Write payload + trailer to temp file */
g_autofree gchar *tmpfile = NULL;
gint fd = g_file_open_tmp ("validity-test-XXXXXX.bin", &tmpfile, NULL);
g_assert_cmpint (fd, >, 0);
g_assert_true (write (fd, payload, payload_len) == (ssize_t) payload_len);
g_assert_true (write (fd, trailer, sizeof (trailer)) == sizeof (trailer));
close (fd);
/* Load and verify */
ValidityDataStore store;
validity_data_store_init (&store);
GError *error = NULL;
gboolean ok = validity_data_load_file (&store, VALIDITY_DATA_INIT,
tmpfile, &error);
g_assert_no_error (error);
g_assert_true (ok);
/* Verify loaded data matches payload (without HMAC) */
gsize len;
const guint8 *data = validity_data_get_bytes (&store, VALIDITY_DATA_INIT, &len);
g_assert_nonnull (data);
g_assert_cmpuint (len, ==, payload_len);
g_assert_cmpmem (data, len, payload, payload_len);
validity_data_store_free (&store);
g_unlink (tmpfile);
}
/* ================================================================
* T8.4: Load file with corrupted HMAC — should fail
* ================================================================ */
static void
test_data_load_corrupt_hmac (void)
{
guint8 payload[] = { 0xCA, 0xFE };
guint8 bad_trailer[32];
memset (bad_trailer, 0xFF, sizeof (bad_trailer));
g_autofree gchar *tmpfile = NULL;
gint fd = g_file_open_tmp ("validity-test-XXXXXX.bin", &tmpfile, NULL);
g_assert_cmpint (fd, >, 0);
g_assert_true (write (fd, payload, sizeof (payload)) == sizeof (payload));
g_assert_true (write (fd, bad_trailer, sizeof (bad_trailer)) == sizeof (bad_trailer));
close (fd);
ValidityDataStore store;
validity_data_store_init (&store);
GError *error = NULL;
gboolean ok = validity_data_load_file (&store, VALIDITY_DATA_INIT,
tmpfile, &error);
g_assert_false (ok);
g_assert_nonnull (error);
g_error_free (error);
/* Tag should not be populated */
gsize len;
const guint8 *data = validity_data_get_bytes (&store, VALIDITY_DATA_INIT, &len);
g_assert_null (data);
validity_data_store_free (&store);
g_unlink (tmpfile);
}
/* ================================================================
* T8.5: Load file too small — should fail
* ================================================================ */
static void
test_data_load_too_small (void)
{
/* File with only 20 bytes — less than HMAC size (32) */
guint8 tiny[20];
memset (tiny, 0x42, sizeof (tiny));
g_autofree gchar *tmpfile = NULL;
gint fd = g_file_open_tmp ("validity-test-XXXXXX.bin", &tmpfile, NULL);
g_assert_cmpint (fd, >, 0);
g_assert_true (write (fd, tiny, sizeof (tiny)) == sizeof (tiny));
close (fd);
ValidityDataStore store;
validity_data_store_init (&store);
GError *error = NULL;
gboolean ok = validity_data_load_file (&store, VALIDITY_DATA_INIT,
tmpfile, &error);
g_assert_false (ok);
g_assert_nonnull (error);
g_error_free (error);
validity_data_store_free (&store);
g_unlink (tmpfile);
}
/* ================================================================
* T8.6: Load nonexistent file — should fail
* ================================================================ */
static void
test_data_load_nonexistent (void)
{
ValidityDataStore store;
validity_data_store_init (&store);
GError *error = NULL;
gboolean ok = validity_data_load_file (&store, VALIDITY_DATA_INIT,
"/nonexistent/path/blob.bin",
&error);
g_assert_false (ok);
g_assert_nonnull (error);
g_error_free (error);
validity_data_store_free (&store);
}
/* ================================================================
* T8.7: Tag enum values are contiguous
* ================================================================ */
static void
test_data_tag_enum (void)
{
g_assert_cmpint (VALIDITY_DATA_INIT, ==, 0);
g_assert_cmpint (VALIDITY_DATA_NUM_TAGS, ==, 11);
g_assert_cmpint (VALIDITY_DATA_FW_PUBKEY_Y, ==,
VALIDITY_DATA_NUM_TAGS - 1);
}
/* ================================================================
* Helper: write a file with valid HMAC-SHA256 trailer
* ================================================================ */
static void
write_hmac_file (const gchar *path,
const guint8 *payload,
gsize payload_len)
{
static const guint8 hmac_key[32] = {
0x6c, 0x69, 0x62, 0x66, 0x70, 0x72, 0x69, 0x6e,
0x74, 0x2d, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69,
0x74, 0x79, 0x2d, 0x64, 0x61, 0x74, 0x61, 0x2d,
0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74,
};
GHmac *hmac = g_hmac_new (G_CHECKSUM_SHA256, hmac_key, sizeof (hmac_key));
g_hmac_update (hmac, payload, payload_len);
guint8 trailer[32];
gsize digest_len = sizeof (trailer);
g_hmac_get_digest (hmac, trailer, &digest_len);
g_hmac_unref (hmac);
gsize total = payload_len + 32;
g_autofree guint8 *buf = g_malloc (total);
memcpy (buf, payload, payload_len);
memcpy (buf + payload_len, trailer, 32);
g_assert_true (g_file_set_contents (path, (const gchar *) buf, total, NULL));
}
/* ================================================================
* T8.8: load_device — missing directory returns DATA_NOT_FOUND
* ================================================================ */
static void
test_data_load_device_missing (void)
{
ValidityDataStore store;
validity_data_store_init (&store);
/* Temporarily override search paths to a non-existent directory */
const gchar *saved = validity_data_search_paths[0];
validity_data_search_paths[0] = "/tmp/nonexistent_validity_data_dir";
validity_data_search_paths[1] = NULL;
GError *error = NULL;
gboolean ok = validity_data_load_device (&store, 0x06cb, 0x009a, &error);
g_assert_false (ok);
g_assert_nonnull (error);
g_assert_cmpint (error->code, ==, FP_DEVICE_ERROR_DATA_NOT_FOUND);
g_error_free (error);
/* Restore */
validity_data_search_paths[0] = saved;
validity_data_search_paths[1] = "/usr/local/share/libfprint/validity";
validity_data_store_free (&store);
}
/* ================================================================
* T8.9: load_device — directory exists but mandatory init.bin missing
* ================================================================ */
static void
test_data_load_device_missing_init (void)
{
/* Create temp dir with the expected subdir but no files */
g_autofree gchar *tmpdir = g_dir_make_tmp ("validity-data-XXXXXX", NULL);
g_assert_nonnull (tmpdir);
g_autofree gchar *devdir = g_build_filename (tmpdir, "06cb_009a", NULL);
g_assert_cmpint (g_mkdir (devdir, 0755), ==, 0);
ValidityDataStore store;
validity_data_store_init (&store);
const gchar *saved0 = validity_data_search_paths[0];
const gchar *saved1 = validity_data_search_paths[1];
validity_data_search_paths[0] = tmpdir;
validity_data_search_paths[1] = NULL;
GError *error = NULL;
gboolean ok = validity_data_load_device (&store, 0x06cb, 0x009a, &error);
g_assert_false (ok);
g_assert_nonnull (error);
g_assert_cmpint (error->code, ==, FP_DEVICE_ERROR_DATA_NOT_FOUND);
g_error_free (error);
validity_data_search_paths[0] = saved0;
validity_data_search_paths[1] = saved1;
validity_data_store_free (&store);
g_rmdir (devdir);
g_rmdir (tmpdir);
}
/* ================================================================
* T8.10: load_device — valid temp dir with init.bin loads correctly
* ================================================================ */
static void
test_data_load_device_valid (void)
{
g_autofree gchar *tmpdir = g_dir_make_tmp ("validity-data-XXXXXX", NULL);
g_assert_nonnull (tmpdir);
g_autofree gchar *devdir = g_build_filename (tmpdir, "06cb_009a", NULL);
g_assert_cmpint (g_mkdir (devdir, 0755), ==, 0);
/* Write a valid init.bin (mandatory) */
guint8 init_payload[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
g_autofree gchar *init_path = g_build_filename (devdir, "init.bin", NULL);
write_hmac_file (init_path, init_payload, sizeof (init_payload));
/* Write a valid db_write_enable.bin (optional) */
guint8 dbe_payload[] = { 0xAA, 0xBB };
g_autofree gchar *dbe_path =
g_build_filename (devdir, "db_write_enable.bin", NULL);
write_hmac_file (dbe_path, dbe_payload, sizeof (dbe_payload));
ValidityDataStore store;
validity_data_store_init (&store);
const gchar *saved0 = validity_data_search_paths[0];
const gchar *saved1 = validity_data_search_paths[1];
validity_data_search_paths[0] = tmpdir;
validity_data_search_paths[1] = NULL;
GError *error = NULL;
gboolean ok = validity_data_load_device (&store, 0x06cb, 0x009a, &error);
g_assert_no_error (error);
g_assert_true (ok);
/* Verify init was loaded */
gsize len;
const guint8 *data = validity_data_get_bytes (&store, VALIDITY_DATA_INIT, &len);
g_assert_nonnull (data);
g_assert_cmpuint (len, ==, sizeof (init_payload));
g_assert_cmpmem (data, len, init_payload, sizeof (init_payload));
/* Verify db_write_enable was loaded */
const guint8 *dbe = validity_data_get_bytes (&store,
VALIDITY_DATA_DB_WRITE_ENABLE,
&len);
g_assert_nonnull (dbe);
g_assert_cmpuint (len, ==, sizeof (dbe_payload));
g_assert_cmpmem (dbe, len, dbe_payload, sizeof (dbe_payload));
/* Optional files not present should be NULL */
const guint8 *reset = validity_data_get_bytes (&store,
VALIDITY_DATA_RESET, &len);
g_assert_null (reset);
g_assert_cmpuint (len, ==, 0);
validity_data_search_paths[0] = saved0;
validity_data_search_paths[1] = saved1;
validity_data_store_free (&store);
g_unlink (init_path);
g_unlink (dbe_path);
g_rmdir (devdir);
g_rmdir (tmpdir);
}
/* ================================================================
* T8.11: load_common — missing files returns DATA_NOT_FOUND
* ================================================================ */
static void
test_data_load_common_missing (void)
{
ValidityDataStore store;
validity_data_store_init (&store);
const gchar *saved0 = validity_data_search_paths[0];
const gchar *saved1 = validity_data_search_paths[1];
validity_data_search_paths[0] = "/tmp/nonexistent_validity_common_dir";
validity_data_search_paths[1] = NULL;
GError *error = NULL;
gboolean ok = validity_data_load_common (&store, &error);
g_assert_false (ok);
g_assert_nonnull (error);
g_assert_cmpint (error->code, ==, FP_DEVICE_ERROR_DATA_NOT_FOUND);
g_error_free (error);
validity_data_search_paths[0] = saved0;
validity_data_search_paths[1] = saved1;
validity_data_store_free (&store);
}
/* ================================================================
* T8.12: load_common — valid temp dir loads all 7 common files
* ================================================================ */
static void
test_data_load_common_valid (void)
{
g_autofree gchar *tmpdir = g_dir_make_tmp ("validity-common-XXXXXX", NULL);
g_assert_nonnull (tmpdir);
/* Write all 7 common files with test payloads */
static const struct
{
const gchar *filename;
ValidityDataTag tag;
guint8 marker;
} files[] = {
{ "partition_sig_standard.bin", VALIDITY_DATA_PARTITION_SIG_STANDARD, 0x01 },
{ "partition_sig_0090.bin", VALIDITY_DATA_PARTITION_SIG_0090, 0x02 },
{ "ca_pubkey.bin", VALIDITY_DATA_CA_PUBKEY, 0x03 },
{ "tls_password.bin", VALIDITY_DATA_TLS_PASSWORD, 0x04 },
{ "gwk_sign.bin", VALIDITY_DATA_GWK_SIGN, 0x05 },
{ "fw_pubkey_x.bin", VALIDITY_DATA_FW_PUBKEY_X, 0x06 },
{ "fw_pubkey_y.bin", VALIDITY_DATA_FW_PUBKEY_Y, 0x07 },
};
g_autofree gchar *paths[7] = { NULL };
for (gsize i = 0; i < G_N_ELEMENTS (files); i++)
{
guint8 payload[4];
memset (payload, files[i].marker, sizeof (payload));
paths[i] = g_build_filename (tmpdir, files[i].filename, NULL);
write_hmac_file (paths[i], payload, sizeof (payload));
}
ValidityDataStore store;
validity_data_store_init (&store);
const gchar *saved0 = validity_data_search_paths[0];
const gchar *saved1 = validity_data_search_paths[1];
validity_data_search_paths[0] = tmpdir;
validity_data_search_paths[1] = NULL;
GError *error = NULL;
gboolean ok = validity_data_load_common (&store, &error);
g_assert_no_error (error);
g_assert_true (ok);
/* Verify each tag was loaded correctly */
for (gsize i = 0; i < G_N_ELEMENTS (files); i++)
{
gsize len;
const guint8 *data = validity_data_get_bytes (&store, files[i].tag, &len);
g_assert_nonnull (data);
g_assert_cmpuint (len, ==, 4);
g_assert_cmpuint (data[0], ==, files[i].marker);
}
validity_data_search_paths[0] = saved0;
validity_data_search_paths[1] = saved1;
/* Cleanup */
validity_data_store_free (&store);
for (gsize i = 0; i < G_N_ELEMENTS (files); i++)
{
g_unlink (paths[i]);
g_free (paths[i]);
paths[i] = NULL;
}
g_rmdir (tmpdir);
}
/* ================================================================
* T8.13: db_write_enable returns NULL without loaded data
* ================================================================ */
static void
test_data_enroll_dbe_missing (void)
{
/* Create a minimal FpiDeviceValidity with empty data stores */
FpiDeviceValidity dev = { 0 };
validity_data_store_init (&dev.device_data);
validity_data_store_init (&dev.common_data);
/* db_write_enable is used in enroll, verify (match), and delete flows.
* When data files are not installed, it must return NULL gracefully. */
gsize len;
const guint8 *dbe = validity_db_get_write_enable_blob (&dev, &len);
g_assert_null (dbe);
g_assert_cmpuint (len, ==, 0);
/* fwext accessor also returns NULL */
const guint8 *fwe = validity_fwext_get_db_write_enable (&dev, &len);
g_assert_null (fwe);
g_assert_cmpuint (len, ==, 0);
/* TLS key pointers should be NULL */
g_assert_null (dev.tls.password);
g_assert_null (dev.tls.gwk_sign);
g_assert_null (dev.tls.fw_pubkey_x);
g_assert_null (dev.tls.fw_pubkey_y);
validity_data_store_free (&dev.device_data);
validity_data_store_free (&dev.common_data);
}
/* ================================================================
* T8.14: db_write_enable returns data when store is populated
* ================================================================ */
static void
test_data_enroll_dbe_loaded (void)
{
/* Create device with loaded db_write_enable */
g_autofree gchar *tmpdir = g_dir_make_tmp ("validity-dbe-XXXXXX", NULL);
g_assert_nonnull (tmpdir);
g_autofree gchar *devdir = g_build_filename (tmpdir, "06cb_009a", NULL);
g_assert_cmpint (g_mkdir (devdir, 0755), ==, 0);
/* Write init.bin (mandatory) and db_write_enable.bin */
guint8 init_data[] = { 0x11 };
g_autofree gchar *init_path = g_build_filename (devdir, "init.bin", NULL);
write_hmac_file (init_path, init_data, sizeof (init_data));
guint8 dbe_data[] = { 0xDB, 0xE0, 0x01, 0x02, 0x03 };
g_autofree gchar *dbe_path =
g_build_filename (devdir, "db_write_enable.bin", NULL);
write_hmac_file (dbe_path, dbe_data, sizeof (dbe_data));
FpiDeviceValidity dev = { 0 };
validity_data_store_init (&dev.device_data);
const gchar *saved0 = validity_data_search_paths[0];
const gchar *saved1 = validity_data_search_paths[1];
validity_data_search_paths[0] = tmpdir;
validity_data_search_paths[1] = NULL;
GError *error = NULL;
gboolean ok = validity_data_load_device (&dev.device_data,
0x06cb, 0x009a, &error);
g_assert_no_error (error);
g_assert_true (ok);
/* Now the enroll/verify consumer should find the data */
gsize len;
const guint8 *dbe = validity_db_get_write_enable_blob (&dev, &len);
g_assert_nonnull (dbe);
g_assert_cmpuint (len, ==, sizeof (dbe_data));
g_assert_cmpmem (dbe, len, dbe_data, sizeof (dbe_data));
/* fwext accessor should also work */
const guint8 *fwe = validity_fwext_get_db_write_enable (&dev, &len);
g_assert_nonnull (fwe);
g_assert_cmpuint (len, ==, sizeof (dbe_data));
validity_data_search_paths[0] = saved0;
validity_data_search_paths[1] = saved1;
validity_data_store_free (&dev.device_data);
g_unlink (init_path);
g_unlink (dbe_path);
g_rmdir (devdir);
g_rmdir (tmpdir);
}
/* ================================================================
* T8.15: load_device rejects corrupt HMAC
* ================================================================ */
static void
test_data_load_device_corrupt (void)
{
g_autofree gchar *tmpdir = g_dir_make_tmp ("validity-corrupt-XXXXXX", NULL);
g_assert_nonnull (tmpdir);
g_autofree gchar *devdir = g_build_filename (tmpdir, "06cb_009a", NULL);
g_assert_cmpint (g_mkdir (devdir, 0755), ==, 0);
/* Write init.bin with bad HMAC */
guint8 bad_file[37]; /* 5 bytes payload + 32 bytes bad HMAC */
memset (bad_file, 0xAA, sizeof (bad_file));
g_autofree gchar *init_path = g_build_filename (devdir, "init.bin", NULL);
g_assert_true (g_file_set_contents (init_path, (const gchar *) bad_file,
sizeof (bad_file), NULL));
ValidityDataStore store;
validity_data_store_init (&store);
const gchar *saved0 = validity_data_search_paths[0];
const gchar *saved1 = validity_data_search_paths[1];
validity_data_search_paths[0] = tmpdir;
validity_data_search_paths[1] = NULL;
GError *error = NULL;
gboolean ok = validity_data_load_device (&store, 0x06cb, 0x009a, &error);
g_assert_false (ok);
g_assert_nonnull (error);
/* HMAC failure reports DATA_INVALID */
g_assert_cmpint (error->code, ==, FP_DEVICE_ERROR_DATA_INVALID);
g_error_free (error);
validity_data_search_paths[0] = saved0;
validity_data_search_paths[1] = saved1;
validity_data_store_free (&store);
g_unlink (init_path);
g_rmdir (devdir);
g_rmdir (tmpdir);
}
/* ================================================================
* 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 */
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 */
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 */
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 */
g_test_add_func ("/validity/verify/struct_separate_fields",
test_struct_separate_fields);
/* R5: del_record command format */
g_test_add_func ("/validity/verify/del_record_format",
test_del_record_format);
/* R6: match_finger allocation */
g_test_add_func ("/validity/verify/match_finger_size",
test_match_finger_size);
/* R7: clear/delete storage SSM states */
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);
/* TLS regression tests */
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);
/* Data loader tests */
g_test_add_func ("/validity/data/store-empty",
test_data_store_empty);
g_test_add_func ("/validity/data/store-double-free",
test_data_store_double_free);
g_test_add_func ("/validity/data/load-valid-hmac",
test_data_load_valid_hmac);
g_test_add_func ("/validity/data/load-corrupt-hmac",
test_data_load_corrupt_hmac);
g_test_add_func ("/validity/data/load-too-small",
test_data_load_too_small);
g_test_add_func ("/validity/data/load-nonexistent",
test_data_load_nonexistent);
g_test_add_func ("/validity/data/tag-enum",
test_data_tag_enum);
g_test_add_func ("/validity/data/load-device-missing",
test_data_load_device_missing);
g_test_add_func ("/validity/data/load-device-missing-init",
test_data_load_device_missing_init);
g_test_add_func ("/validity/data/load-device-valid",
test_data_load_device_valid);
g_test_add_func ("/validity/data/load-common-missing",
test_data_load_common_missing);
g_test_add_func ("/validity/data/load-common-valid",
test_data_load_common_valid);
g_test_add_func ("/validity/data/enroll-dbe-missing",
test_data_enroll_dbe_missing);
g_test_add_func ("/validity/data/enroll-dbe-loaded",
test_data_enroll_dbe_loaded);
g_test_add_func ("/validity/data/load-device-corrupt",
test_data_load_device_corrupt);
return g_test_run ();
}