validity: Iteration 8 — Final Polish

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.
This commit is contained in:
Leonardo Francisco 2026-04-06 16:12:26 -04:00 committed by lewohart
parent 52606ccebc
commit 94bbb5fa2c
7 changed files with 432 additions and 277 deletions

View file

@ -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`

View file

@ -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);

View file

@ -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);

View file

@ -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)

View file

@ -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
* ================================================================ */

View file

@ -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',

View file

@ -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 <glib.h>
#include <string.h>
#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 ();
}