From 94bbb5fa2c60a25f9385c9769fef51bd0bb32acb Mon Sep 17 00:00:00 2001 From: Leonardo Francisco Date: Mon, 6 Apr 2026 16:12:26 -0400 Subject: [PATCH] =?UTF-8?q?validity:=20Iteration=208=20=E2=80=94=20Final?= =?UTF-8?q?=20Polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add init_hardcoded and init_clean_slate transmission to the open SSM. Four new states (OPEN_SEND/RECV_INIT_HARDCODED, OPEN_SEND/RECV_INIT_ CLEAN_SLATE) between GET_FW_INFO and UPLOAD_FWEXT, matching the python-validity send_init() flow. init_hardcoded is always sent; clean_slate only when fwext is not loaded. Skipped in emulation mode. Remove dead crt_hardcoded[] (420 bytes, G_GNUC_UNUSED) from validity_tls.c — this CA cert data now lives exclusively in validity_pair_constants.inc. Expose enrollment response parser for unit testing: - EnrollmentUpdateResult struct and ENROLLMENT_MAGIC_LEN moved to validity.h - parse_enrollment_update_response() and enrollment_update_result_clear() no longer static Remove in-tree doc/ directory — documentation lives in ../validity-artifacts/docs/. Tests: 9 new enrollment parser test cases, 0 regressions. Result: 41 OK, 0 Fail, 2 Skipped. --- .../validity/doc/07-device-pairing-and-hal.md | 217 ------------- libfprint/drivers/validity/validity.c | 109 ++++++- libfprint/drivers/validity/validity.h | 18 ++ libfprint/drivers/validity/validity_enroll.c | 17 +- libfprint/drivers/validity/validity_tls.c | 42 --- tests/meson.build | 14 + tests/test-validity-enroll.c | 292 ++++++++++++++++++ 7 files changed, 432 insertions(+), 277 deletions(-) delete mode 100644 libfprint/drivers/validity/doc/07-device-pairing-and-hal.md create mode 100644 tests/test-validity-enroll.c diff --git a/libfprint/drivers/validity/doc/07-device-pairing-and-hal.md b/libfprint/drivers/validity/doc/07-device-pairing-and-hal.md deleted file mode 100644 index d3fd44ea..00000000 --- a/libfprint/drivers/validity/doc/07-device-pairing-and-hal.md +++ /dev/null @@ -1,217 +0,0 @@ -# Iteration 7: Device Pairing & Hardware Abstraction Layer (HAL) - -## Overview - -Iteration 7 introduces two major subsystems: - -1. **Hardware Abstraction Layer (HAL)** — A lookup table that maps device PIDs to - their per-device blobs (init_hardcoded, clean_slate, reset_blob, db_write_enable) - and flash layout (partition table + RSA signature). - -2. **Device Pairing** — A 30-state SSM that performs first-time pairing when the - sensor has no TLS flash partitions. This involves ECDH key exchange, certificate - generation, partition table flashing, TLS handshake, erase cycles, and writing - the TLS flash image. - -## Supported Devices - -| VID | PID | Dev Type | Clean Slate | Notes | -|--------|--------|--------------------|-------------|------------------| -| 0x138a | 0x0090 | VALIDITY_DEV_90 | No | Smaller blobs | -| 0x138a | 0x0097 | VALIDITY_DEV_97 | Yes | | -| 0x06cb | 0x009a | VALIDITY_DEV_9A | Yes | | -| 0x138a | 0x009d | VALIDITY_DEV_9D | Yes | | - -## Architecture - -### HAL (`validity_hal.h`, `validity_hal.c`) - -``` -ValidityDeviceDesc (per-PID) -├── vid, pid, dev_type -├── init_hardcoded / init_hardcoded_len -├── init_clean_slate / init_clean_slate_len (NULL for 0090) -├── reset_blob / reset_blob_len -├── db_write_enable / db_write_enable_len -└── flash_layout → ValidityFlashLayout - ├── partitions[] → ValidityPartition { id, type, access_lvl, offset, size } - ├── num_partitions - ├── partition_sig / partition_sig_len (256 bytes RSA-2048) -``` - -Lookup functions: -- `validity_hal_device_lookup(ValidityHalDeviceType)` — by enum type -- `validity_hal_device_lookup_by_pid(guint16 pid)` — by USB PID - -### Pairing SSM (`validity_pair.h`, `validity_pair.c`) - -The pairing SSM runs as a child of the open SSM (`OPEN_PAIR` state). It is -skipped when: (a) emulation mode, (b) no firmware extension loaded, or -(c) `num_partitions > 0` (already paired). - -#### SSM Phases - -**Phase 1 — Raw USB (pre-TLS):** -- `GET_FLASH_INFO` → parse flash IC params + partition count -- Check if pairing needed (0 partitions) -- Send reset blob -- Generate ECDH key pair (P-256) -- Build & send partition flash command (0x4f) -- CMD 0x50 (get ECDH server response) -- Extract server cert, ECDH blob, derive keys, encrypt private key -- Cleanup commands (0x1a series) - -**Phase 2 — TLS Handshake:** -- Start TLS handshake child SSM - -**Phase 3 — TLS Erase Loop:** -- Erase 5 partitions in order: {1, 2, 5, 6, 4} -- Each: DB write enable → erase → cleanup - -**Phase 4 — TLS Write Flash:** -- DB write enable → write 4096-byte TLS flash image → cleanup - -**Phase 5 — Reboot:** -- Send reboot command (0x05 0x02 0x00) -- Returns `FP_DEVICE_ERROR_REMOVED` so fprintd re-opens device - -### TLS Flash Image Format - -``` -Offset Content -0x000 Block 0: [id:2LE=0][size:2LE=1][SHA256:32][body:1=0x01] -0x025 Block 4: [id:2LE=4][size:2LE][SHA256:32][priv_blob] - Block 3: [id:2LE=3][size:2LE][SHA256:32][server_cert] - Block 5: [id:2LE=5][size:2LE][SHA256:32][ecdh_blob] - Block 1: [id:2LE=1][size:2LE][SHA256:32][ca_cert(420B)] - Block 2: [id:2LE=2][size:2LE][SHA256:32][client_cert(444B)] - Block 6: [id:2LE=6][size:2LE][SHA256:32][16 zero bytes] - Padding: 0xff to 4096 bytes total -``` - -## Files - -| File | Purpose | -|----------------------------|--------------------------------------| -| `validity_hal.h` | HAL types, lookup function decls | -| `validity_hal.c` | HAL device table, blob includes | -| `validity_pair.h` | Pair state, SSM enum, helper decls | -| `validity_pair.c` | Pair SSM runner + all helpers | -| `validity_pair_constants.inc` | CA cert, partition signatures | -| `validity_blobs_0090.inc` | Blobs for PID 0090 | -| `validity_blobs_0097.inc` | Blobs for PID 0097 | -| `validity_blobs_009a.inc` | Blobs for PID 009a | -| `validity_blobs_009d.inc` | Blobs for PID 009d | - -## Build & Test Runbook - -### Build - -``` -cd /home/lewohart/src/libfprint -meson setup builddir # first time only -ninja -C builddir -``` - -### Run All Tests - -``` -meson test -C builddir --print-errorlogs -``` - -Expected output: -``` -Ok: 40 -Fail: 0 -Skipped: 2 -``` - -The 2 skipped tests are `virtual-image` and `virtual-device` (require -`FPRINT_VIRTUAL_IMAGE` / `FPRINT_VIRTUAL_DEVICE` environment variables). - -### Run Only Unit Tests - -``` -meson test -C builddir --suite unit-tests --print-errorlogs -``` - -Expected output (8 unit test suites): -``` -unit-tests - libfprint:validity-tls OK -unit-tests - libfprint:validity-fwext OK -unit-tests - libfprint:validity-sensor OK -unit-tests - libfprint:validity-capture OK -unit-tests - libfprint:validity-db OK -unit-tests - libfprint:validity-verify OK -unit-tests - libfprint:validity-hal OK -unit-tests - libfprint:validity-pair OK -unit-tests - libfprint:fpi-assembling OK -unit-tests - libfprint:fpi-ssm OK -unit-tests - libfprint:fpi-device OK -``` - -### Run Individual Tests - -``` -# HAL tests (10 test cases) -meson test -C builddir validity-hal --print-errorlogs - -# Pairing tests (14 test cases) -meson test -C builddir validity-pair --print-errorlogs -``` - -### Verbose Single Test - -``` -./builddir/tests/test-validity-hal --verbose -./builddir/tests/test-validity-pair --verbose -``` - -## Test Coverage - -### HAL Tests (`test-validity-hal.c`) — 10 cases - -| Test | Validates | -|------------------------------------|----------------------------------------| -| `lookup-by-type` | All 4 device types resolve | -| `lookup-by-pid` | All 4 PIDs resolve | -| `lookup-invalid-type` | Invalid type returns NULL | -| `lookup-invalid-pid` | Invalid PID returns NULL | -| `blobs-present` | Non-null blobs with non-zero sizes | -| `pid-0090-specifics` | 0090 has no clean_slate blob | -| `clean-slate-presence` | 0097/009a/009d have clean_slate | -| `flash-layout-valid` | Partition count, sig, offsets ordering | -| `blob-sizes` | Known blob sizes per device | -| `lookup-consistency` | by_type and by_pid return same pointer | - -### Pairing Tests (`test-validity-pair.c`) — 14 cases - -| Test | Validates | -|------------------------------------|----------------------------------------| -| `parse-flash-info-valid` | Correct parsing of flash IC params | -| `parse-flash-info-needs-pairing` | 0 partitions = needs pairing | -| `parse-flash-info-too-short` | Short buffer returns FALSE | -| `serialize-partition` | Output format + embedded SHA-256 | -| `make-cert-size` | Certificate is exactly 444 bytes | -| `make-cert-deterministic` | Same inputs → same header bytes | -| `encrypt-key-structure` | Output is 161 bytes, prefix 0x02 | -| `encrypt-key-hmac-valid` | HMAC over iv+ct matches stored HMAC | -| `build-partition-flash-cmd` | 0x4f prefix, header structure | -| `build-tls-flash-size` | Exactly 4096 bytes, 0xff padding | -| `build-tls-flash-blocks` | Block 0 and block 4 in correct order | -| `state-lifecycle` | Init zeroes all fields, free is safe | -| `state-free-with-resources` | Free releases EVP_PKEY + g_malloc'd | -| `encrypt-key-different-inputs` | Different keys → different ciphertext | - -## Integration Points - -- **Open SSM**: `OPEN_PAIR` state between `OPEN_UPLOAD_FWEXT` and `OPEN_TLS_READ_FLASH` -- **Close**: `validity_pair_state_free()` called in `dev_close` -- **DB operations**: `validity_db_get_write_enable_blob()` now takes `guint dev_type` - and uses HAL lookup (replaces hardcoded blob include) -- **FWExt operations**: `validity_fwext_get_db_write_enable()` uses HAL lookup by PID - -## Migration Notes - -- Deleted `validity_blob_dbe_009a.inc` — replaced by per-device blobs in HAL -- `validity_db.c` and `validity_fwext.c` now depend on `validity_hal.h` diff --git a/libfprint/drivers/validity/validity.c b/libfprint/drivers/validity/validity.c index 74d491d4..61d328c5 100644 --- a/libfprint/drivers/validity/validity.c +++ b/libfprint/drivers/validity/validity.c @@ -164,9 +164,9 @@ err_close: * 1) GET_VERSION (0x01) * 2) UNKNOWN_INIT (0x19) * 3) GET_FW_INFO (0x43 0x02) — check if fwext loaded - * 4) Upload firmware extension (if not loaded) - * 5) Send init_hardcoded blob - * 6) If no fwext: send init_hardcoded_clean_slate blob + * 4) Send init_hardcoded blob (per-device, via HAL) + * 5) If no fwext: send init_hardcoded_clean_slate blob + * 6) Upload firmware extension (if not loaded) */ typedef enum { @@ -176,6 +176,10 @@ typedef enum { OPEN_RECV_CMD19, OPEN_SEND_GET_FW_INFO, OPEN_RECV_GET_FW_INFO, + OPEN_SEND_INIT_HARDCODED, + OPEN_RECV_INIT_HARDCODED, + OPEN_SEND_INIT_CLEAN_SLATE, + OPEN_RECV_INIT_CLEAN_SLATE, OPEN_UPLOAD_FWEXT, OPEN_PAIR, OPEN_TLS_READ_FLASH, @@ -412,6 +416,105 @@ open_run_state (FpiSsm *ssm, fw_info_recv_cb, NULL); break; + case OPEN_SEND_INIT_HARDCODED: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + const ValidityDeviceDesc *desc = + validity_hal_device_lookup (self->dev_type); + + if (!desc || !desc->init_hardcoded) + { + fp_warn ("No init_hardcoded blob for dev_type %u — skipping", + self->dev_type); + fpi_ssm_jump_to_state (ssm, OPEN_UPLOAD_FWEXT); + return; + } + + /* In emulation mode, skip raw USB blobs */ + if (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") == 0) + { + fp_dbg ("Emulation mode — skipping init_hardcoded"); + fpi_ssm_jump_to_state (ssm, OPEN_UPLOAD_FWEXT); + return; + } + + fp_dbg ("Sending init_hardcoded (%zu bytes)", desc->init_hardcoded_len); + transfer = fpi_usb_transfer_new (dev); + transfer->short_is_error = TRUE; + transfer->ssm = ssm; + fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_OUT, + desc->init_hardcoded_len); + memcpy (transfer->buffer, desc->init_hardcoded, + desc->init_hardcoded_len); + fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT, NULL, + fpi_ssm_usb_transfer_cb, NULL); + } + break; + + case OPEN_RECV_INIT_HARDCODED: + transfer = fpi_usb_transfer_new (dev); + transfer->ssm = ssm; + fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_IN, + VALIDITY_MAX_TRANSFER_LEN); + fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT, NULL, + fpi_ssm_usb_transfer_cb, NULL); + break; + + case OPEN_SEND_INIT_CLEAN_SLATE: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + /* clean_slate is only sent when fwext is NOT loaded */ + if (self->fwext_loaded) + { + fpi_ssm_next_state (ssm); + return; + } + + const ValidityDeviceDesc *desc = + validity_hal_device_lookup (self->dev_type); + + if (!desc || !desc->init_clean_slate) + { + fp_dbg ("No init_clean_slate blob — skipping"); + fpi_ssm_next_state (ssm); + return; + } + + fp_info ("Fwext not loaded — sending init_clean_slate (%zu bytes)", + desc->init_clean_slate_len); + transfer = fpi_usb_transfer_new (dev); + transfer->short_is_error = TRUE; + transfer->ssm = ssm; + fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_OUT, + desc->init_clean_slate_len); + memcpy (transfer->buffer, desc->init_clean_slate, + desc->init_clean_slate_len); + fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT, NULL, + fpi_ssm_usb_transfer_cb, NULL); + } + break; + + case OPEN_RECV_INIT_CLEAN_SLATE: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + /* If fwext loaded, we skipped the send — just advance */ + if (self->fwext_loaded) + { + fpi_ssm_next_state (ssm); + return; + } + + transfer = fpi_usb_transfer_new (dev); + transfer->ssm = ssm; + fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_IN, + VALIDITY_MAX_TRANSFER_LEN); + fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT, NULL, + fpi_ssm_usb_transfer_cb, NULL); + } + break; + case OPEN_UPLOAD_FWEXT: { FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); diff --git a/libfprint/drivers/validity/validity.h b/libfprint/drivers/validity/validity.h index 63fc5095..a8a763d8 100644 --- a/libfprint/drivers/validity/validity.h +++ b/libfprint/drivers/validity/validity.h @@ -264,6 +264,24 @@ struct _FpiDeviceValidity /* Enrollment SSM (validity_enroll.c) */ void validity_enroll (FpDevice *device); +/* Enrollment response parsing — exposed for unit testing */ +#define ENROLLMENT_MAGIC_LEN 0x38 + +typedef struct +{ + guint8 *header; + gsize header_len; + guint8 *template_data; + gsize template_len; + guint8 *tid; + gsize tid_len; +} EnrollmentUpdateResult; + +void enrollment_update_result_clear (EnrollmentUpdateResult *r); +gboolean parse_enrollment_update_response (const guint8 *data, + gsize data_len, + EnrollmentUpdateResult *result); + /* Verify/Identify SSMs (validity_verify.c) */ void validity_verify (FpDevice *device); void validity_identify (FpDevice *device); diff --git a/libfprint/drivers/validity/validity_enroll.c b/libfprint/drivers/validity/validity_enroll.c index 8f146a97..e4b2647d 100644 --- a/libfprint/drivers/validity/validity_enroll.c +++ b/libfprint/drivers/validity/validity_enroll.c @@ -42,9 +42,6 @@ #include "validity.h" #include "vcsfw_protocol.h" -/* Magic length for enrollment response parsing (hardcoded in DLL) */ -#define ENROLLMENT_MAGIC_LEN 0x38 - /* ================================================================ * Interrupt helpers — read from EP 0x83 * ================================================================ */ @@ -147,17 +144,7 @@ start_interrupt_wait (FpiDeviceValidity *self, * tag 0 → template, tag 1 → header, tag 3 → tid (enrollment complete) * ================================================================ */ -typedef struct -{ - guint8 *header; - gsize header_len; - guint8 *template_data; - gsize template_len; - guint8 *tid; - gsize tid_len; -} EnrollmentUpdateResult; - -static void +void enrollment_update_result_clear (EnrollmentUpdateResult *r) { g_clear_pointer (&r->header, g_free); @@ -166,7 +153,7 @@ enrollment_update_result_clear (EnrollmentUpdateResult *r) memset (r, 0, sizeof (*r)); } -static gboolean +gboolean parse_enrollment_update_response (const guint8 *data, gsize data_len, EnrollmentUpdateResult *result) diff --git a/libfprint/drivers/validity/validity_tls.c b/libfprint/drivers/validity/validity_tls.c index bdabe1e7..28e5e386 100644 --- a/libfprint/drivers/validity/validity_tls.c +++ b/libfprint/drivers/validity/validity_tls.c @@ -67,48 +67,6 @@ static const guint8 fw_pubkey_y[32] = { 0x6e, 0x0d, 0xc5, 0xbe, 0xb6, 0xf8, 0x38, 0xa8 }; -/* Hardcoded CA certificate — used during device pairing (init_flash) and - * TLS flash persistence (make_tls_flash, block ID 5). Not needed for the - * normal TLS handshake; kept here for Iteration 6. */ -static const guint8 crt_hardcoded[] G_GNUC_UNUSED = { - 0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x60, 0xd2, 0x27, - 0x3e, 0x3c, 0xce, 0x3b, 0xf6, 0xb0, 0x53, 0xcc, 0xb0, 0x06, 0x1d, 0x65, - 0xbc, 0x86, 0x98, 0x76, 0x55, 0xbd, 0xeb, 0xb3, 0xe7, 0x93, 0x3a, 0xaa, - 0xd8, 0x35, 0xc6, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x96, 0xc2, 0x98, 0xd8, 0x45, 0x39, 0xa1, 0xf4, 0xa0, 0x33, 0xeb, 0x2d, - 0x81, 0x7d, 0x03, 0x77, 0xf2, 0x40, 0xa4, 0x63, 0xe5, 0xe6, 0xbc, 0xf8, - 0x47, 0x42, 0x2c, 0xe1, 0xf2, 0xd1, 0x17, 0x6b, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xf5, 0x51, 0xbf, 0x37, 0x68, 0x40, 0xb6, 0xcb, - 0xce, 0x5e, 0x31, 0x6b, 0x57, 0x33, 0xce, 0x2b, 0x16, 0x9e, 0x0f, 0x7c, - 0x4a, 0xeb, 0xe7, 0x8e, 0x9b, 0x7f, 0x1a, 0xfe, 0xe2, 0x42, 0xe3, 0x4f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x25, 0x63, 0xfc, - 0xc2, 0xca, 0xb9, 0xf3, 0x84, 0x9e, 0x17, 0xa7, 0xad, 0xfa, 0xe6, 0xbc, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - - - /* ================================================================ * TLS PRF (P_SHA256) — Standard TLS 1.2 PRF with HMAC-SHA256 * ================================================================ */ diff --git a/tests/meson.build b/tests/meson.build index 722253fa..8dfebcd7 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -413,6 +413,20 @@ if 'validity' in supported_drivers env: envs, ) + # Validity enrollment response parsing unit tests + validity_enroll_test = executable('test-validity-enroll', + sources: 'test-validity-enroll.c', + dependencies: [ libfprint_private_dep ], + c_args: common_cflags, + link_with: libfprint_drivers, + install: false, + ) + test('validity-enroll', + validity_enroll_test, + suite: ['unit-tests'], + env: envs, + ) + # Validity HAL unit tests validity_hal_test = executable('test-validity-hal', sources: 'test-validity-hal.c', diff --git a/tests/test-validity-enroll.c b/tests/test-validity-enroll.c new file mode 100644 index 00000000..68755534 --- /dev/null +++ b/tests/test-validity-enroll.c @@ -0,0 +1,292 @@ +/* + * Unit tests for enrollment response parsing + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + */ + +#include +#include + +#include "fpi-byte-utils.h" +#include "drivers/validity/validity.h" + +/* ================================================================ + * Helper: build a tagged block + * [tag:2LE][len:2LE][padding:MAGIC_LEN][payload:len] + * Total block size = 4 + MAGIC_LEN + len = MAGIC_LEN + len + 4 + * Wait — re-read the parser: + * tag(2LE) | len(2LE) => block_size = MAGIC_LEN + len + * so the full block is [tag:2][len:2] + body[MAGIC_LEN + len] + * No — looking at the code: pos + 4 reads tag+len, then + * block_size = MAGIC_LEN + len, and the block starts at data[pos]. + * Template: data[pos .. pos + block_size]. + * Header: data[pos + MAGIC_LEN .. pos + MAGIC_LEN + len]. + * Advance: pos += block_size. + * + * Actually re-reading more carefully: + * tag = data[pos], len = data[pos+2] + * block_size = MAGIC_LEN + len + * template = data[pos .. pos + block_size] + * So the 4 bytes of tag+len are INSIDE the block_size. + * MAGIC_LEN = 0x38 = 56 which is > 4, so tag+len fit inside. + * + * To build test data: write tag(2LE) at offset 0, len(2LE) at + * offset 2, then (MAGIC_LEN - 4) padding bytes, then len payload bytes. + * Total = MAGIC_LEN + len. + * ================================================================ */ +static guint8 * +build_block (guint16 tag, const guint8 *payload, guint16 payload_len, + gsize *out_len) +{ + gsize block_size = ENROLLMENT_MAGIC_LEN + payload_len; + guint8 *buf = g_malloc0 (block_size); + + FP_WRITE_UINT16_LE (buf, tag); + FP_WRITE_UINT16_LE (buf + 2, payload_len); + + if (payload && payload_len > 0) + memcpy (buf + ENROLLMENT_MAGIC_LEN, payload, payload_len); + + *out_len = block_size; + return buf; +} + +/* ================================================================ + * 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); + + g_assert_true (ok); + g_assert_null (result.header); + g_assert_null (result.template_data); + g_assert_null (result.tid); +} + +/* ================================================================ + * 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 *data = build_block (0, payload, sizeof (payload), + &block_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, block_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 *data = build_block (1, payload, sizeof (payload), + &block_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, block_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 *data = build_block (3, payload, sizeof (payload), + &block_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, block_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 */ + gsize total = tmpl_len + hdr_len + tid_len; + g_autofree guint8 *data = g_malloc (total); + memcpy (data, tmpl, tmpl_len); + memcpy (data + tmpl_len, hdr, hdr_len); + memcpy (data + tmpl_len + hdr_len, tid, tid_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, total, &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) +{ + guint8 payload[] = { 0xAA }; + gsize block_len; + g_autofree guint8 *data = build_block (0, payload, sizeof (payload), + &block_len); + + /* Pass data_len shorter than block_size so the block can't be read */ + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, 10, &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 *data = build_block (42, payload, sizeof (payload), + &block_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, block_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 *data = build_block (1, NULL, 0, &block_len); + + EnrollmentUpdateResult result; + gboolean ok = parse_enrollment_update_response (data, block_len, &result); + + g_assert_true (ok); + /* Tag 1 with len=0: header should be NULL (len > 0 check in parser) */ + g_assert_null (result.header); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/validity/enroll/parse-empty", + test_parse_empty); + g_test_add_func ("/validity/enroll/parse-template-block", + test_parse_template_block); + g_test_add_func ("/validity/enroll/parse-header-block", + test_parse_header_block); + g_test_add_func ("/validity/enroll/parse-tid-block", + test_parse_tid_block); + g_test_add_func ("/validity/enroll/parse-multiple-blocks", + test_parse_multiple_blocks); + g_test_add_func ("/validity/enroll/parse-truncated", + test_parse_truncated); + g_test_add_func ("/validity/enroll/parse-unknown-tag", + test_parse_unknown_tag); + g_test_add_func ("/validity/enroll/result-clear", + test_result_clear); + g_test_add_func ("/validity/enroll/parse-zero-length-payload", + test_parse_zero_length_payload); + + return g_test_run (); +}