libfprint/libfprint/drivers/validity/validity_pair.c

1316 lines
44 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.

/*
* Device pairing for Validity/Synaptics VCSFW fingerprint sensors
*
* Handles pairing of uninitialized devices: flash partitioning,
* ECDH key exchange, certificate creation, and TLS flash persistence.
*
* 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.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define FP_COMPONENT "validity"
#include "drivers_api.h"
#include "fpi-byte-utils.h"
#include "validity.h"
#include "validity_pair.h"
#include "validity_tls.h"
#include "vcsfw_protocol.h"
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/core_names.h>
#include <openssl/param_build.h>
#include <string.h>
/* Include CA cert and other pairing constants */
#include "validity_pair_constants.inc"
/* Hardcoded password for HS_KEY_PAIR_GEN derivation.
* From python-validity tls.py: password_hardcoded */
static const guint8 password_hardcoded[] = {
0x71, 0x7c, 0xd7, 0x2d, 0x09, 0x62, 0xbc, 0x4a,
0x28, 0x46, 0x13, 0x8d, 0xbb, 0x2c, 0x24, 0x19,
0x25, 0x12, 0xa7, 0x64, 0x07, 0x06, 0x5f, 0x38,
0x38, 0x46, 0x13, 0x9d, 0x4b, 0xec, 0x20, 0x33,
};
/* ================================================================
* Pairing state management
* ================================================================ */
void
validity_pair_state_init (ValidityPairState *state)
{
memset (state, 0, sizeof (*state));
}
void
validity_pair_state_free (ValidityPairState *state)
{
g_clear_pointer (&state->client_key, EVP_PKEY_free);
g_clear_pointer (&state->server_cert, g_free);
g_clear_pointer (&state->ecdh_blob, g_free);
g_clear_pointer (&state->priv_blob, g_free);
}
/* ================================================================
* Flash info parsing (CMD 0x3e response)
*
* Response format (after 2-byte status):
* [jid0:2LE][jid1:2LE][blocks:2LE][unknown0:2LE]
* [blocksize:2LE][unknown1:2LE][partition_count:2LE]
* [partition_entries: count * 12 bytes each]
* ================================================================ */
#define FLASH_INFO_HEADER_SIZE 14 /* 7 × guint16 */
gboolean
validity_pair_parse_flash_info (const guint8 *data,
gsize data_len,
ValidityFlashIcParams *ic_out,
guint16 *num_partitions_out)
{
if (!data || data_len < FLASH_INFO_HEADER_SIZE)
return FALSE;
guint16 jid0 = FP_READ_UINT16_LE (data + 0);
guint16 jid1 = FP_READ_UINT16_LE (data + 2);
guint16 blocks = FP_READ_UINT16_LE (data + 4);
guint16 blocksize = FP_READ_UINT16_LE (data + 8);
guint16 pcnt = FP_READ_UINT16_LE (data + 12);
(void) jid0;
(void) jid1;
/* Flash IC params for CMD 0x4f: we need size, sector_size, erase_cmd.
* The actual IC lookup is done by the device firmware — we just pass
* the size info through in serialize_flash_params format. */
ic_out->size = (guint32) blocks * (guint32) blocksize;
/* Default sector size and erase command for the common flash ICs
* used in these devices (W25Q80B, MX25V8035F).
* python-validity looks these up from flash_ic_table, but we only
* need (size, sector_size, sector_erase_cmd) for serialize_flash_params. */
ic_out->sector_size = 0x1000;
ic_out->sector_erase_cmd = 0x20;
*num_partitions_out = pcnt;
fp_dbg ("Flash info: size=%u (blocks=%u × bs=%u), partitions=%u",
ic_out->size, blocks, blocksize, pcnt);
return TRUE;
}
/* ================================================================
* Partition serialization
*
* Each entry: [id:1][type:1][access:2LE][offset:4LE][size:4LE]
* + 4 zero bytes
* + SHA-256(12-byte entry) = 32 bytes
* Total: 48 bytes per partition
* ================================================================ */
void
validity_pair_serialize_partition (const ValidityPartition *part,
guint8 *out)
{
guint8 entry[12];
entry[0] = part->id;
entry[1] = part->type;
FP_WRITE_UINT16_LE (entry + 2, part->access_lvl);
FP_WRITE_UINT32_LE (entry + 4, part->offset);
FP_WRITE_UINT32_LE (entry + 8, part->size);
/* Copy 12-byte entry to output */
memcpy (out, entry, 12);
/* 4 zero bytes */
memset (out + 12, 0, 4);
/* SHA-256 of the 12-byte entry */
g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA256);
g_checksum_update (checksum, entry, 12);
gsize hash_len = 32;
g_checksum_get_digest (checksum, out + 16, &hash_len);
}
/* ================================================================
* Header builder: [id:2LE][len:2LE][body]
* Matches python-validity with_hdr()
* ================================================================ */
static guint8 *
build_header (guint16 id, const guint8 *body, gsize body_len, gsize *out_len)
{
gsize total = 4 + body_len;
guint8 *buf = g_malloc (total);
FP_WRITE_UINT16_LE (buf, id);
FP_WRITE_UINT16_LE (buf + 2, (guint16) body_len);
if (body && body_len > 0)
memcpy (buf + 4, body, body_len);
*out_len = total;
return buf;
}
/* ================================================================
* Flash IC params serialization
*
* Format: [size:4LE][sector_size:4LE][pad:2][erase_cmd:1][pad:1]
* Matches python-validity serialize_flash_params()
* ================================================================ */
static void
serialize_flash_params (const ValidityFlashIcParams *ic, guint8 *out)
{
FP_WRITE_UINT32_LE (out, ic->size);
FP_WRITE_UINT32_LE (out + 4, ic->sector_size);
out[8] = 0;
out[9] = 0;
out[10] = ic->sector_erase_cmd;
out[11] = 0;
}
/* ================================================================
* HS key derivation
*
* The handshake signing key is derived from the hardcoded password
* using the same PRF as TLS:
* key = password[0:16]
* seed = password[16:32] + 0xaa 0xaa
* hs_key = PRF(key, "HS_KEY_PAIR_GEN" + seed, 32)
*
* This key is used to ECDSA-sign the client certificate.
* ================================================================ */
static EVP_PKEY *
derive_hs_signing_key (void)
{
const guint8 *key = password_hardcoded;
guint8 prf_seed[15 + 16 + 2]; /* "HS_KEY_PAIR_GEN" + password[16:32] + 0xaa*2 */
guint8 hs_key_bytes[32];
/* Build PRF seed: label + password_tail + 0xaa padding */
memcpy (prf_seed, "HS_KEY_PAIR_GEN", 15);
memcpy (prf_seed + 15, password_hardcoded + 16, 16);
prf_seed[31] = 0xaa;
prf_seed[32] = 0xaa;
/* PRF(key=password[0:16], seed=prf_seed, output_len=32) */
validity_tls_prf (key, 16, prf_seed, 33, hs_key_bytes, 32);
/* Convert to big-endian scalar (python-validity does [::-1] reversal) */
guint8 be_scalar[32];
for (int i = 0; i < 32; i++)
be_scalar[i] = hs_key_bytes[31 - i];
/* Build EC private key from scalar */
BIGNUM *priv_bn = BN_bin2bn (be_scalar, 32, NULL);
if (!priv_bn)
return NULL;
/* Derive public key from private scalar on P-256 */
EC_GROUP *group = EC_GROUP_new_by_curve_name (NID_X9_62_prime256v1);
EC_POINT *pub_point = EC_POINT_new (group);
EC_POINT_mul (group, pub_point, priv_bn, NULL, NULL, NULL);
/* Extract public key coordinates */
BIGNUM *pub_x = BN_new ();
BIGNUM *pub_y = BN_new ();
EC_POINT_get_affine_coordinates (group, pub_point, pub_x, pub_y, NULL);
guint8 pub_x_bytes[32], pub_y_bytes[32];
BN_bn2binpad (pub_x, pub_x_bytes, 32);
BN_bn2binpad (pub_y, pub_y_bytes, 32);
/* Build EVP_PKEY via OSSL_PARAM_BLD */
OSSL_PARAM_BLD *bld = OSSL_PARAM_BLD_new ();
OSSL_PARAM_BLD_push_utf8_string (bld, OSSL_PKEY_PARAM_GROUP_NAME, "prime256v1", 0);
guint8 pub_uncompressed[65];
pub_uncompressed[0] = 0x04;
memcpy (pub_uncompressed + 1, pub_x_bytes, 32);
memcpy (pub_uncompressed + 33, pub_y_bytes, 32);
OSSL_PARAM_BLD_push_octet_string (bld, OSSL_PKEY_PARAM_PUB_KEY, pub_uncompressed, 65);
OSSL_PARAM_BLD_push_BN (bld, OSSL_PKEY_PARAM_PRIV_KEY, priv_bn);
OSSL_PARAM *params = OSSL_PARAM_BLD_to_param (bld);
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_name (NULL, "EC", NULL);
EVP_PKEY *pkey = NULL;
EVP_PKEY_fromdata_init (ctx);
EVP_PKEY_fromdata (ctx, &pkey, EVP_PKEY_KEYPAIR, params);
EVP_PKEY_CTX_free (ctx);
OSSL_PARAM_free (params);
OSSL_PARAM_BLD_free (bld);
BN_free (pub_x);
BN_free (pub_y);
EC_POINT_free (pub_point);
EC_GROUP_free (group);
BN_free (priv_bn);
return pkey;
}
/* ================================================================
* Client certificate builder
*
* Format (444 bytes total):
* [0x17:4LE][0x20:4LE]
* [public_x:32 LE][zeros:36]
* [public_y:32 LE][zeros:76]
* [sig_len:4LE][DER signature]
* [zero-pad to 444 bytes]
*
* The certificate body (before signature) is signed with the HS key.
* ================================================================ */
/* Certificate body size: 8 + 32 + 36 + 32 + 76 = 184 bytes */
#define CERT_BODY_SIZE 184
guint8 *
validity_pair_make_cert (const guint8 *client_public_x,
const guint8 *client_public_y,
gsize *out_len)
{
guint8 body[CERT_BODY_SIZE];
memset (body, 0, sizeof (body));
FP_WRITE_UINT32_LE (body, 0x17);
FP_WRITE_UINT32_LE (body + 4, 0x20);
memcpy (body + 8, client_public_x, 32);
/* 36 zero bytes at offset 40..75 */
memcpy (body + 76, client_public_y, 32);
/* 76 zero bytes at offset 108..183 */
/* Sign body with HS key (ECDSA + SHA-256) */
EVP_PKEY *hs_key = derive_hs_signing_key ();
if (!hs_key)
{
fp_warn ("Failed to derive HS signing key");
return NULL;
}
guint8 sig_buf[128];
size_t sig_len = sizeof (sig_buf);
EVP_MD_CTX *md_ctx = EVP_MD_CTX_new ();
EVP_DigestSignInit (md_ctx, NULL, EVP_sha256 (), NULL, hs_key);
EVP_DigestSignUpdate (md_ctx, body, sizeof (body));
if (EVP_DigestSignFinal (md_ctx, sig_buf, &sig_len) != 1)
{
fp_warn ("ECDSA signing failed");
EVP_MD_CTX_free (md_ctx);
EVP_PKEY_free (hs_key);
return NULL;
}
EVP_MD_CTX_free (md_ctx);
EVP_PKEY_free (hs_key);
/* Build output: body + sig_len(4LE) + sig + zero-pad to 444 */
guint8 *cert = g_malloc0 (VALIDITY_CLIENT_CERT_SIZE);
memcpy (cert, body, sizeof (body));
gsize offset = sizeof (body);
FP_WRITE_UINT32_LE (cert + offset, (guint32) sig_len);
offset += 4;
if (offset + sig_len <= VALIDITY_CLIENT_CERT_SIZE)
memcpy (cert + offset, sig_buf, sig_len);
*out_len = VALIDITY_CLIENT_CERT_SIZE;
return cert;
}
/* ================================================================
* Private key encryption
*
* Encrypts the ECDH client private key for flash storage:
* plaintext = x(32LE) + y(32LE) + d(32LE) + PKCS7 padding to 16-byte block
* iv = random 16 bytes
* ciphertext = AES-256-CBC(psk_encryption_key, iv, padded_plaintext)
* blob = 0x02 + iv + ciphertext + HMAC-SHA256(psk_validation_key, iv+ciphertext)
* ================================================================ */
guint8 *
validity_pair_encrypt_key (const guint8 *client_private,
const guint8 *client_public_x,
const guint8 *client_public_y,
const guint8 *psk_encryption_key,
const guint8 *psk_validation_key,
gsize *out_len)
{
/* Build plaintext: x + y + d = 96 bytes */
guint8 plaintext[96 + 16]; /* + max PKCS7 padding */
memcpy (plaintext, client_public_x, 32);
memcpy (plaintext + 32, client_public_y, 32);
memcpy (plaintext + 64, client_private, 32);
/* PKCS7 pad to 16-byte boundary: 96 bytes → pad_len = 16 */
guint8 pad_len = 16 - (96 % 16);
if (pad_len == 0)
pad_len = 16;
memset (plaintext + 96, pad_len, pad_len);
gsize padded_len = 96 + pad_len;
/* Generate random IV */
guint8 iv[VALIDITY_ENCRYPTED_KEY_IV_SIZE];
if (RAND_bytes (iv, sizeof (iv)) != 1)
{
fp_warn ("Failed to generate random IV");
return NULL;
}
/* AES-256-CBC encrypt */
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new ();
EVP_CIPHER_CTX_set_padding (ctx, 0); /* We handle padding manually */
if (EVP_EncryptInit_ex (ctx, EVP_aes_256_cbc (), NULL,
psk_encryption_key, iv) != 1)
{
EVP_CIPHER_CTX_free (ctx);
return NULL;
}
guint8 ciphertext[112 + 16]; /* padded_len + possible block */
int ct_len = 0, final_len = 0;
EVP_EncryptUpdate (ctx, ciphertext, &ct_len, plaintext, (int) padded_len);
EVP_EncryptFinal_ex (ctx, ciphertext + ct_len, &final_len);
ct_len += final_len;
EVP_CIPHER_CTX_free (ctx);
/* Build blob: 0x02 + iv(16) + ciphertext + HMAC-SHA256(iv + ciphertext) */
gsize iv_ct_len = sizeof (iv) + ct_len;
gsize blob_len = 1 + iv_ct_len + 32; /* prefix + iv+ct + hmac */
guint8 *blob = g_malloc (blob_len);
blob[0] = VALIDITY_ENCRYPTED_KEY_PREFIX;
memcpy (blob + 1, iv, sizeof (iv));
memcpy (blob + 1 + sizeof (iv), ciphertext, ct_len);
/* HMAC-SHA256 over iv + ciphertext */
unsigned int hmac_len = 32;
HMAC (EVP_sha256 (),
psk_validation_key, 32,
blob + 1, iv_ct_len,
blob + 1 + iv_ct_len, &hmac_len);
*out_len = blob_len;
/* Clear sensitive plaintext from stack */
OPENSSL_cleanse (plaintext, sizeof (plaintext));
return blob;
}
/* ================================================================
* CMD 0x4f (PARTITION_FLASH) command builder
*
* Payload format:
* [0x4f][0x00 0x00][0x00 0x00] — 5-byte command prefix
* [hdr 0: flash IC params (12 bytes)] — serialize_flash_params
* [hdr 1: partition table + RSA signature] — serialized partitions + sig
* [hdr 5: client certificate (444 bytes)] — make_cert output
* [hdr 3: CA certificate] — hardcoded
*
* Each header: [id:2LE][body_len:2LE][body]
* ================================================================ */
guint8 *
validity_pair_build_partition_flash_cmd (const ValidityFlashIcParams *flash_ic,
const ValidityFlashLayout *layout,
const guint8 *client_public_x,
const guint8 *client_public_y,
gsize *out_len)
{
/* Build flash IC params body (hdr 0) */
guint8 ic_body[12];
serialize_flash_params (flash_ic, ic_body);
gsize hdr0_len;
g_autofree guint8 *hdr0 = build_header (VALIDITY_HDR_FLASH_IC,
ic_body, sizeof (ic_body),
&hdr0_len);
/* Build partition table body (hdr 1):
* [partition entries (48 bytes each)] + [RSA signature (256 bytes)] */
gsize ptbl_body_len = (layout->num_partitions * VALIDITY_PARTITION_ENTRY_SIZE) +
layout->partition_sig_len;
g_autofree guint8 *ptbl_body = g_malloc0 (ptbl_body_len);
for (gsize i = 0; i < layout->num_partitions; i++)
validity_pair_serialize_partition (&layout->partitions[i],
ptbl_body + (i * VALIDITY_PARTITION_ENTRY_SIZE));
memcpy (ptbl_body + (layout->num_partitions * VALIDITY_PARTITION_ENTRY_SIZE),
layout->partition_sig, layout->partition_sig_len);
gsize hdr1_len;
g_autofree guint8 *hdr1 = build_header (VALIDITY_HDR_PARTITION_TABLE,
ptbl_body, ptbl_body_len,
&hdr1_len);
/* Build client certificate (hdr 5) */
gsize cert_len;
g_autofree guint8 *cert = validity_pair_make_cert (client_public_x,
client_public_y,
&cert_len);
if (!cert)
return NULL;
gsize hdr5_len;
g_autofree guint8 *hdr5 = build_header (VALIDITY_HDR_CLIENT_CERT,
cert, cert_len,
&hdr5_len);
/* CA certificate (hdr 3) — from auto-generated constants */
gsize ca_cert_len = sizeof (ca_cert_hardcoded);
gsize hdr3_len;
g_autofree guint8 *hdr3 = build_header (VALIDITY_HDR_CA_CERT,
ca_cert_hardcoded, ca_cert_len,
&hdr3_len);
/* Assemble: [4f 00 00 00 00] + hdr0 + hdr1 + hdr5 + hdr3 */
gsize cmd_prefix_len = 5;
gsize total = cmd_prefix_len + hdr0_len + hdr1_len + hdr5_len + hdr3_len;
guint8 *cmd = g_malloc0 (total);
cmd[0] = 0x4f;
/* bytes 1..4 are zero (already from g_malloc0) */
gsize offset = cmd_prefix_len;
memcpy (cmd + offset, hdr0, hdr0_len);
offset += hdr0_len;
memcpy (cmd + offset, hdr1, hdr1_len);
offset += hdr1_len;
memcpy (cmd + offset, hdr5, hdr5_len);
offset += hdr5_len;
memcpy (cmd + offset, hdr3, hdr3_len);
*out_len = total;
return cmd;
}
/* ================================================================
* TLS flash image builder
*
* Builds the 4096-byte flash image that contains all TLS data.
* Format: sequence of blocks [id:2LE][size:2LE][SHA256:32][body]
* padded with 0xff to 4096 bytes.
*
* Block order (from python-validity make_tls_flash):
* block 0: single zero byte (empty marker)
* block 4: encrypted private key (priv_blob)
* block 3: server certificate (from partition_flash response)
* block 5: CA certificate (hardcoded)
* block 1: 256 zero bytes (empty placeholder)
* block 2: 256 zero bytes (empty placeholder)
* block 6: ECDH blob (from CMD 0x50 response)
* Remaining: 0xff padding to 0x1000
* ================================================================ */
#define TLS_FLASH_IMAGE_SIZE 0x1000
static gsize
append_flash_block (guint8 *buf, gsize offset, guint16 id,
const guint8 *body, gsize body_len)
{
/* Header: [id:2LE][size:2LE] */
FP_WRITE_UINT16_LE (buf + offset, id);
FP_WRITE_UINT16_LE (buf + offset + 2, (guint16) body_len);
offset += 4;
/* SHA-256 of body */
g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA256);
g_checksum_update (checksum, body, body_len);
gsize hash_len = 32;
g_checksum_get_digest (checksum, buf + offset, &hash_len);
offset += 32;
/* Body */
memcpy (buf + offset, body, body_len);
offset += body_len;
return offset;
}
guint8 *
validity_pair_build_tls_flash (const ValidityPairState *state,
gsize *out_len)
{
guint8 *buf = g_malloc (TLS_FLASH_IMAGE_SIZE);
/* Fill with 0xff initially (padding) */
memset (buf, 0xff, TLS_FLASH_IMAGE_SIZE);
guint8 zero_byte = 0x00;
guint8 empty_block[256];
memset (empty_block, 0, sizeof (empty_block));
gsize offset = 0;
/* Block 0: empty marker */
offset = append_flash_block (buf, offset, 0, &zero_byte, 1);
/* Block 4: encrypted private key */
if (state->priv_blob && state->priv_blob_len > 0)
offset = append_flash_block (buf, offset, 4,
state->priv_blob, state->priv_blob_len);
/* Block 3: server certificate */
if (state->server_cert && state->server_cert_len > 0)
offset = append_flash_block (buf, offset, 3,
state->server_cert, state->server_cert_len);
/* Block 5: CA certificate (hardcoded) */
offset = append_flash_block (buf, offset, 5,
ca_cert_hardcoded, sizeof (ca_cert_hardcoded));
/* Block 1: empty placeholder (256 zeros) */
offset = append_flash_block (buf, offset, 1, empty_block, sizeof (empty_block));
/* Block 2: empty placeholder (256 zeros) */
offset = append_flash_block (buf, offset, 2, empty_block, sizeof (empty_block));
/* Block 6: ECDH blob */
if (state->ecdh_blob && state->ecdh_blob_len > 0)
offset = append_flash_block (buf, offset, 6,
state->ecdh_blob, state->ecdh_blob_len);
/* Remaining bytes stay 0xff from initial memset */
(void) offset;
*out_len = TLS_FLASH_IMAGE_SIZE;
return buf;
}
/* ================================================================
* Pairing SSM runner
*
* Drives the full pairing sequence through USB commands.
* Requires self->pair_state to be initialized.
* Uses raw USB for pre-TLS phase and TLS-wrapped for post-TLS.
* ================================================================ */
void
validity_pair_run_state (FpiSsm *ssm,
FpDevice *dev)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
ValidityPairState *ps = &self->pair_state;
switch (fpi_ssm_get_cur_state (ssm))
{
/* ---- Phase 1: Pre-TLS (raw USB) ---- */
case PAIR_GET_FLASH_INFO:
{
/* CMD 0x3e: GET_FLASH_INFO — ask how many partitions exist */
guint8 cmd[] = { VCSFW_CMD_GET_FLASH_INFO };
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_GET_FLASH_INFO_RECV:
/* Response already in self->cmd_response_* from vcsfw_cmd_send sub-SSM.
* Fall through to CHECK_NEEDED which parses it. */
fpi_ssm_next_state (ssm);
break;
case PAIR_CHECK_NEEDED:
{
/* Parse CMD 0x3e response: status(2) + data */
if (!self->cmd_response_data ||
self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("GET_FLASH_INFO failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
if (!validity_pair_parse_flash_info (self->cmd_response_data,
self->cmd_response_len,
&ps->flash_ic,
&ps->num_partitions))
{
fp_warn ("Failed to parse flash info");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
if (ps->num_partitions > 0)
{
fp_info ("Flash has %u partitions — verifying TLS keys",
ps->num_partitions);
/* Read flash partition 1 to check if TLS keys exist.
* If they do, pairing is complete. If not, we re-pair. */
fpi_ssm_next_state (ssm);
return;
}
fp_info ("Flash has 0 partitions — device needs pairing");
/* Look up device descriptor */
ps->dev_desc = validity_hal_device_lookup (self->dev_type);
if (!ps->dev_desc)
{
fp_warn ("No HAL descriptor for dev_type=%u", self->dev_type);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
return;
}
/* No partitions — skip TLS verify, go straight to pairing */
fpi_ssm_jump_to_state (ssm, PAIR_SEND_RESET_BLOB);
}
break;
case PAIR_VERIFY_TLS_SEND:
{
/* Read flash partition 1 (TLS cert store) to verify keys exist */
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);
FP_WRITE_UINT32_LE (&cmd[5], 0x0000);
FP_WRITE_UINT32_LE (&cmd[9], 0x1000);
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_VERIFY_TLS_RECV:
{
/* Check if TLS flash has valid key data */
gboolean have_keys = FALSE;
if (self->cmd_response_status == VCSFW_STATUS_OK &&
self->cmd_response_data && self->cmd_response_len > 6)
{
guint32 flash_sz = FP_READ_UINT32_LE (self->cmd_response_data);
const guint8 *flash_data = self->cmd_response_data + 6;
gsize flash_avail = self->cmd_response_len - 6;
if (flash_sz > flash_avail)
flash_sz = flash_avail;
/* Quick check: scan for block IDs 3 (cert), 4 (privkey), 6 (ecdh) */
const guint8 *pos = flash_data;
gsize remaining = flash_sz;
gboolean found_priv = FALSE, found_ecdh = FALSE, found_cert = FALSE;
while (remaining >= 36) /* header(4) + hash(32) */
{
guint16 block_id = FP_READ_UINT16_LE (pos);
guint16 block_size = FP_READ_UINT16_LE (pos + 2);
if (block_id == 0xFFFF)
break;
pos += 36; /* skip header + hash */
remaining -= 36;
if (block_size > remaining)
break;
if (block_id == 4)
found_priv = TRUE;
if (block_id == 6)
found_ecdh = TRUE;
if (block_id == 3)
found_cert = TRUE;
pos += block_size;
remaining -= block_size;
}
have_keys = found_priv && found_ecdh && found_cert;
}
if (have_keys)
{
fp_info ("TLS keys verified on flash — pairing not needed");
fpi_ssm_jump_to_state (ssm, PAIR_DONE);
return;
}
fp_info ("TLS keys missing from flash — starting pairing");
/* Look up device descriptor */
ps->dev_desc = validity_hal_device_lookup (self->dev_type);
if (!ps->dev_desc)
{
fp_warn ("No HAL descriptor for dev_type=%u", self->dev_type);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case PAIR_SEND_RESET_BLOB:
{
/* Send reset_blob via raw USB (python-validity: usb.cmd(reset_blob)) */
if (!ps->dev_desc->reset_blob || ps->dev_desc->reset_blob_len == 0)
{
fp_warn ("No reset_blob available for this device");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
return;
}
vcsfw_cmd_send (self, ssm,
ps->dev_desc->reset_blob,
ps->dev_desc->reset_blob_len, NULL);
}
break;
case PAIR_SEND_RESET_BLOB_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("reset_blob failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case PAIR_GENERATE_KEYS:
{
/* Generate ECDH P-256 key pair */
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, &ps->client_key);
EVP_PKEY_CTX_free (pctx);
if (!ps->client_key)
{
fp_warn ("ECDH key generation failed");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
return;
}
fp_info ("Generated ECDH client key pair");
fpi_ssm_next_state (ssm);
}
break;
case PAIR_PARTITION_FLASH_SEND:
{
/* Extract public key coordinates (little-endian) */
BIGNUM *pub_x_bn = NULL, *pub_y_bn = NULL;
EVP_PKEY_get_bn_param (ps->client_key, OSSL_PKEY_PARAM_EC_PUB_X, &pub_x_bn);
EVP_PKEY_get_bn_param (ps->client_key, OSSL_PKEY_PARAM_EC_PUB_Y, &pub_y_bn);
guint8 pub_x_be[32], pub_y_be[32];
guint8 pub_x_le[32], pub_y_le[32];
BN_bn2binpad (pub_x_bn, pub_x_be, 32);
BN_bn2binpad (pub_y_bn, pub_y_be, 32);
BN_free (pub_x_bn);
BN_free (pub_y_bn);
/* Convert big-endian → little-endian (python-validity uses LE) */
for (int i = 0; i < 32; i++)
{
pub_x_le[i] = pub_x_be[31 - i];
pub_y_le[i] = pub_y_be[31 - i];
}
/* Build CMD 0x4f */
gsize cmd_len;
g_autofree guint8 *cmd = validity_pair_build_partition_flash_cmd (
&ps->flash_ic,
ps->dev_desc->flash_layout,
pub_x_le, pub_y_le,
&cmd_len);
if (!cmd)
{
fp_warn ("Failed to build partition_flash command");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
return;
}
fp_info ("Sending partition_flash (CMD 0x4f): %" G_GSIZE_FORMAT " bytes",
cmd_len);
/* NOTE: partition_flash is sent via raw USB (TLS not yet active).
* python-validity sends it through tls.cmd() which falls back to
* raw USB when secure_rx/secure_tx are false. */
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case PAIR_PARTITION_FLASH_RECV:
{
if (self->cmd_response_status == 0x0404)
{
/* 0x0404 = partitions already exist (half-initialized device).
* Factory reset will wipe flash, then reboot. Next device open
* will start with a clean slate and full pairing will succeed. */
fp_info ("Flash already partitioned (0x0404) — factory reset needed");
fpi_ssm_next_state (ssm);
return;
}
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("partition_flash failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* Response: [cert_len:4LE][cert_data:cert_len][...] */
if (self->cmd_response_data && self->cmd_response_len >= 4)
{
guint32 cert_len = FP_READ_UINT32_LE (self->cmd_response_data);
if (cert_len <= self->cmd_response_len - 4)
{
ps->server_cert = g_memdup2 (self->cmd_response_data + 4,
cert_len);
ps->server_cert_len = cert_len;
fp_info ("Received server certificate: %u bytes", cert_len);
}
}
/* Skip factory reset states — go straight to CMD50 */
fpi_ssm_jump_to_state (ssm, PAIR_CMD50_SEND);
}
break;
case PAIR_FACTORY_RESET_SEND:
{
/* CMD 0x10 + 0x61 zero bytes: wipes flash partition table.
* python-validity: usb.cmd(b'\x10' + b'\0' * 0x61) */
guint8 cmd[98];
memset (cmd, 0, sizeof (cmd));
cmd[0] = 0x10;
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_FACTORY_RESET_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
fp_warn ("Factory reset cmd 0x10 status=0x%04x",
self->cmd_response_status);
else
fp_info ("Factory reset complete — rebooting sensor");
/* Reboot; next device open will pair from clean state */
fpi_ssm_jump_to_state (ssm, PAIR_REBOOT_SEND);
}
break;
case PAIR_CMD50_SEND:
{
/* CMD 0x50: get ECDH server parameters
* python-validity: usb.cmd(unhex('50')) */
guint8 cmd[] = { VCSFW_CMD_GET_ECDH };
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_CMD50_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("CMD 0x50 failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case PAIR_CMD50_PROCESS:
{
/* Response: [length:4LE][zeros:...][ecdh_blob:400]
* python-validity:
* l, = unpack('<L', rsp[:4])
* zeroes, rsp = rsp[4:-400], rsp[-400:] */
if (!self->cmd_response_data || self->cmd_response_len < 404)
{
fp_warn ("CMD 0x50 response too short: %" G_GSIZE_FORMAT,
self->cmd_response_len);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
guint32 resp_len = FP_READ_UINT32_LE (self->cmd_response_data);
const guint8 *ecdh_data = self->cmd_response_data +
self->cmd_response_len - 400;
fp_info ("CMD 0x50 response: declared_len=%u, actual=%" G_GSIZE_FORMAT,
resp_len, self->cmd_response_len);
/* Store ECDH blob: handle_ecdh stores raw blob, extracts pubkey.
* We store it for TLS flash persistence and set up tls.ecdh_q
* via the existing validity_tls code path. */
ps->ecdh_blob = g_memdup2 (ecdh_data, 400);
ps->ecdh_blob_len = 400;
/* Parse ECDH blob to extract server public key.
* This sets self->tls.ecdh_q and ecdh_blob. */
self->tls.ecdh_blob = g_memdup2 (ecdh_data, 400);
self->tls.ecdh_blob_len = 400;
/* Extract X,Y coordinates from ECDH blob for ecdh_q.
* Format: [header:8][x:32 LE][padding:36][y:32 LE][...] */
const guint8 *x_le = ecdh_data + TLS_ECDH_X_OFFSET;
const guint8 *y_le = ecdh_data + TLS_ECDH_Y_OFFSET;
guint8 x_be[32], y_be[32];
for (int i = 0; i < 32; i++)
{
x_be[i] = x_le[31 - i];
y_be[i] = y_le[31 - i];
}
/* Build ECDH server public key */
OSSL_PARAM_BLD *bld = OSSL_PARAM_BLD_new ();
OSSL_PARAM_BLD_push_utf8_string (bld, OSSL_PKEY_PARAM_GROUP_NAME,
"prime256v1", 0);
guint8 pub_uncompressed[65];
pub_uncompressed[0] = 0x04;
memcpy (pub_uncompressed + 1, x_be, 32);
memcpy (pub_uncompressed + 33, y_be, 32);
OSSL_PARAM_BLD_push_octet_string (bld, OSSL_PKEY_PARAM_PUB_KEY,
pub_uncompressed, 65);
OSSL_PARAM *params = OSSL_PARAM_BLD_to_param (bld);
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_name (NULL, "EC", NULL);
EVP_PKEY_fromdata_init (ctx);
EVP_PKEY_fromdata (ctx, &self->tls.ecdh_q, EVP_PKEY_PUBLIC_KEY, params);
EVP_PKEY_CTX_free (ctx);
OSSL_PARAM_free (params);
OSSL_PARAM_BLD_free (bld);
if (!self->tls.ecdh_q)
{
fp_warn ("Failed to build ECDH server public key");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* Encrypt client private key → priv_blob (handle_priv)
* python-validity: tls.handle_priv(encrypt_key(client_private, client_public)) */
/* First, derive PSK if not already done */
validity_tls_derive_psk (&self->tls);
/* Extract private key scalar (little-endian) */
BIGNUM *priv_bn = NULL;
EVP_PKEY_get_bn_param (ps->client_key, OSSL_PKEY_PARAM_PRIV_KEY, &priv_bn);
guint8 priv_be[32], priv_le[32];
BN_bn2binpad (priv_bn, priv_be, 32);
BN_free (priv_bn);
for (int i = 0; i < 32; i++)
priv_le[i] = priv_be[31 - i];
/* Get public key LE coords */
BIGNUM *pub_x_bn = NULL, *pub_y_bn = NULL;
EVP_PKEY_get_bn_param (ps->client_key, OSSL_PKEY_PARAM_EC_PUB_X, &pub_x_bn);
EVP_PKEY_get_bn_param (ps->client_key, OSSL_PKEY_PARAM_EC_PUB_Y, &pub_y_bn);
guint8 pub_x_be2[32], pub_y_be2[32];
guint8 pub_x_le2[32], pub_y_le2[32];
BN_bn2binpad (pub_x_bn, pub_x_be2, 32);
BN_bn2binpad (pub_y_bn, pub_y_be2, 32);
BN_free (pub_x_bn);
BN_free (pub_y_bn);
for (int i = 0; i < 32; i++)
{
pub_x_le2[i] = pub_x_be2[31 - i];
pub_y_le2[i] = pub_y_be2[31 - i];
}
gsize priv_blob_len;
ps->priv_blob = validity_pair_encrypt_key (priv_le, pub_x_le2, pub_y_le2,
self->tls.psk_encryption_key,
self->tls.psk_validation_key,
&priv_blob_len);
ps->priv_blob_len = priv_blob_len;
/* Also store in TLS state for handle_priv path */
self->tls.priv_blob = g_memdup2 (ps->priv_blob, ps->priv_blob_len);
self->tls.priv_blob_len = ps->priv_blob_len;
/* Store server cert in TLS state too */
if (ps->server_cert)
{
self->tls.tls_cert = g_memdup2 (ps->server_cert, ps->server_cert_len);
self->tls.tls_cert_len = ps->server_cert_len;
}
/* Set priv_key — the TLS handshake needs the actual EC private key
* (EVP_PKEY*) to sign cert_verify. We have it as ps->client_key. */
if (self->tls.priv_key)
EVP_PKEY_free (self->tls.priv_key);
self->tls.priv_key = EVP_PKEY_dup (ps->client_key);
OPENSSL_cleanse (priv_le, sizeof (priv_le));
OPENSSL_cleanse (priv_be, sizeof (priv_be));
fp_info ("ECDH exchange complete, private key encrypted");
fpi_ssm_next_state (ssm);
}
break;
case PAIR_CLEANUPS_SEND:
{
/* CMD 0x1a: call_cleanups after CMD 0x50
* python-validity: call_cleanups() in finally block */
guint8 cmd[] = { VCSFW_CMD_CLEANUPS };
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_CLEANUPS_RECV:
{
/* Ignore "nothing to commit" (0x0491) status */
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
fp_warn ("cleanups failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
/* ---- Phase 2: TLS handshake ---- */
case PAIR_TLS_HANDSHAKE:
{
/* Establish TLS session — python-validity: tls.open()
* Uses subsm: tls_ssm completion/failure propagates to pair SSM.
* NOTE: do NOT overwrite self->open_ssm here — it must remain
* pointing to the open SSM for pair_ssm_done to work. */
FpiSsm *tls_ssm = fpi_ssm_new (dev,
validity_tls_handshake_run_state,
TLS_HS_NUM_STATES);
fpi_ssm_start_subsm (ssm, tls_ssm);
}
break;
/* ---- Phase 3: Flash erase (TLS-wrapped) ---- */
case PAIR_ERASE_DBE_SEND:
{
/* Send db_write_enable before each erase
* python-validity: erase_flash() → tls.cmd(db_write_enable) */
fp_info ("Erasing partition %u (step %u/%u)",
pair_erase_partition_ids[ps->erase_step],
ps->erase_step + 1,
(guint) VALIDITY_PAIR_NUM_ERASE_STEPS);
vcsfw_tls_cmd_send (self, ssm,
ps->dev_desc->db_write_enable,
ps->dev_desc->db_write_enable_len, NULL);
}
break;
case PAIR_ERASE_DBE_RECV:
/* db_write_enable response — proceed regardless of status */
fpi_ssm_next_state (ssm);
break;
case PAIR_ERASE_SEND:
{
/* CMD 0x3f: erase partition */
guint8 cmd[2];
cmd[0] = VCSFW_CMD_ERASE_FLASH;
cmd[1] = pair_erase_partition_ids[ps->erase_step];
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_ERASE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
fp_warn ("erase partition %u failed: status=0x%04x",
pair_erase_partition_ids[ps->erase_step],
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
case PAIR_ERASE_CLEAN_SEND:
{
/* CMD 0x1a: call_cleanups after erase */
guint8 cmd[] = { VCSFW_CMD_CLEANUPS };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_ERASE_CLEAN_RECV:
{
/* Ignore "nothing to commit" */
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
fp_warn ("post-erase cleanups status=0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
case PAIR_ERASE_LOOP:
{
ps->erase_step++;
if (ps->erase_step < VALIDITY_PAIR_NUM_ERASE_STEPS)
{
fpi_ssm_jump_to_state (ssm, PAIR_ERASE_DBE_SEND);
return;
}
fpi_ssm_next_state (ssm);
}
break;
/* ---- Phase 4: Write TLS flash (TLS-wrapped) ---- */
case PAIR_WRITE_DBE_SEND:
{
/* db_write_enable before write_flash */
vcsfw_tls_cmd_send (self, ssm,
ps->dev_desc->db_write_enable,
ps->dev_desc->db_write_enable_len, NULL);
}
break;
case PAIR_WRITE_DBE_RECV:
fpi_ssm_next_state (ssm);
break;
case PAIR_WRITE_FLASH_SEND:
{
/* Build TLS flash image */
gsize flash_len;
g_autofree guint8 *flash_data =
validity_pair_build_tls_flash (ps, &flash_len);
/* CMD 0x41: WRITE_FLASH
* Format: [0x41][partition:1][flag:1][reserved:2][offset:4LE][size:4LE][data]
* python-validity: pack('<BBBHLL', 0x41, partition, 1, 0, addr, len(buf)) + buf */
gsize cmd_len = 1 + 1 + 1 + 2 + 4 + 4 + flash_len;
guint8 *cmd = g_malloc0 (cmd_len);
cmd[0] = VCSFW_CMD_WRITE_FLASH;
cmd[1] = 1; /* partition 1 (cert store) */
cmd[2] = 1; /* flag */
/* cmd[3..4] = 0 (reserved, from g_malloc0) */
FP_WRITE_UINT32_LE (cmd + 5, 0); /* offset = 0 */
FP_WRITE_UINT32_LE (cmd + 9, (guint32) flash_len);
memcpy (cmd + 13, flash_data, flash_len);
fp_info ("Writing TLS flash: %" G_GSIZE_FORMAT " bytes to partition 1",
flash_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case PAIR_WRITE_FLASH_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("write_flash failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case PAIR_WRITE_CLEAN_SEND:
{
guint8 cmd[] = { VCSFW_CMD_CLEANUPS };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_WRITE_CLEAN_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
fp_warn ("post-write cleanups status=0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
/* ---- Phase 5: Reboot ---- */
case PAIR_REBOOT_SEND:
{
/* Reboot: 0x05 0x02 0x00 (python-validity: tls.cmd(unhex('050200')))
* Use raw USB — TLS may not be established (factory reset path). */
guint8 cmd[] = { VCSFW_CMD_REBOOT, 0x02, 0x00 };
ps->reboot_pending = TRUE;
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_REBOOT_RECV:
{
fp_info ("Reboot command sent — device will re-enumerate");
fpi_ssm_next_state (ssm);
}
break;
case PAIR_DONE:
fpi_ssm_mark_completed (ssm);
break;
}
}
FpiSsm *
validity_pair_ssm_new (FpDevice *dev)
{
return fpi_ssm_new (dev, validity_pair_run_state, PAIR_NUM_STATES);
}