mirror of
https://gitlab.freedesktop.org/libfprint/libfprint.git
synced 2026-05-11 13:08:13 +02:00
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:
parent
52606ccebc
commit
94bbb5fa2c
7 changed files with 432 additions and 277 deletions
|
|
@ -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`
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* ================================================================ */
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
292
tests/test-validity-enroll.c
Normal file
292
tests/test-validity-enroll.c
Normal 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 ();
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue