diff --git a/.gitignore b/.gitignore index 6be133c..f2896b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ # venv Python cục bộ của tools/ (tạo: python3 -m venv tools/.venv && tools/.venv/bin/pip install -r tools/vectors/requirements.txt -r tools/schemas/requirements.txt) tools/.venv/ +# venv riêng cho từng thư mục con của tools/ (tools/setup-cpace-refs.sh, một venv cạnh mỗi requirements*.txt) +tools/**/.venv/ +# Văn bản gốc (.txt) tải từ IETF datatracker để chép vector — không commit (tools/setup-cpace-refs.sh) +tools/vectors/.refs/ __pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c27feb..c411063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [0.2.0] — 2026-10-30 + +### Added + +- **Phase 7 (offline connectivity):** test vectors for the Bluetooth L2CAP link-security handshake (Noise prologue, + challenge, `noise1`, `noise2`, `bt-link.json`), the pre-auth/transport frame codec (channel IDs, record length bounds, + `bt-frame.json`), advertisement service data (rotating hint, `bt-advertisement.json`) and CPace-X25519-SHA512 + password-authenticated key exchange (`cpace.json`, draft vectors plus HandLive-specific cases); Bluetooth pairing + envelopes (`bt-pairing.json`) — all consumed by `core/crypto`/`core/protocol`/`core/transport` on Android and + `HLCrypto`/`HLProtocol`/`HLTransport` on Apple; `tools/setup-cpace-refs.sh` fetches the CPace draft and verifies the + vectors in per-tool venvs. `envelope.schema.json`/`error.schema.json`: new envelope types `direct` and `bulk` (both + already in the Android/Apple `MessageType` enum). + +- UI strings for the device label "Connected via Bluetooth" (`status.connected_bluetooth(_to)`, CONN-05, shown by the + status indicator on Android/Mac/iOS) and the Mac Bluetooth permission prompt text (`infoplist.bluetooth_always`, wired + into `macOS/Info.plist`'s `NSBluetoothAlwaysUsageDescription`). + +### Specs + +- Catalog entries added for 01-setup-settings.md SET-01/SET-03 that no Android/Apple UI calls yet: the Bluetooth + permission-primer screen (`permission.bluetooth_title/body/body_mac`), the Mac "Bluetooth off"/"Bluetooth denied" + status lines and their settings button (`status.bluetooth_off`, `status.bluetooth_denied`, + `common.open_bluetooth_settings`), and the Settings feature-card label (`settings.bluetooth_link`). +- `error.schema.json`'s new `DIRECT_WIFI_UNAVAILABLE` code (0.4.6): schema only, no code returns or handles it yet — + Wi-Fi Direct itself (SSID/passphrase format, per-connection bulk stream keys `K_bulk`) has no code or test vectors on + either platform. +- `bt_seen`/`bt_lost`/`wifi_assoc` bench events (`tools/bench/reconnect_time.py`): the trigger logic is in place, but + only `tools/ble-spike` and `Tools/BLESpike` emit these events today — the shipped app does not yet. + ## [0.1.1] — 2026-10-22 ### Changed diff --git a/schemas/envelope.schema.json b/schemas/envelope.schema.json index a981520..cf04a75 100644 --- a/schemas/envelope.schema.json +++ b/schemas/envelope.schema.json @@ -27,7 +27,7 @@ }, "$defs": { "type": { - "description": "Nhóm tin (0.7.1): tập đã chốt cộng session, camera, web.", + "description": "Nhóm tin (0.7.1): tập đã chốt cộng session, camera, web, direct, bulk (Phase 7, 0.4.6/0.4.7).", "enum": [ "clipboard", "sms", @@ -39,7 +39,9 @@ "capability", "session", "camera", - "web" + "web", + "direct", + "bulk" ] } } diff --git a/schemas/error.schema.json b/schemas/error.schema.json index a294faf..27567ac 100644 --- a/schemas/error.schema.json +++ b/schemas/error.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://handlive.app/schemas/v1/error.schema.json", "title": "HandLive — đối tượng lỗi trong ack", - "description": "00-common-specs 0.5.1 (ack lỗi) và 0.8.1 (mã lỗi ứng dụng). Mã lỗi relay HTTP (0.8.2) không thuộc schema này.", + "description": "00-common-specs 0.5.1 (ack lỗi) và 0.8.1 (mã lỗi ứng dụng, đủ cả Phase 7 DIRECT_WIFI_UNAVAILABLE). Mã lỗi relay HTTP (0.8.2) không thuộc schema này.", "type": "object", "required": ["code", "message"], "additionalProperties": false, @@ -70,7 +70,8 @@ "USB_ADB_UNAUTHORIZED", "USB_ADB_UNAVAILABLE", "MAC_EXTENSION_NOT_ACTIVE", - "MAC_MIC_DRIVER_MISSING" + "MAC_MIC_DRIVER_MISSING", + "DIRECT_WIFI_UNAVAILABLE" ] } } diff --git a/strings/ui-strings.json b/strings/ui-strings.json index 6165f89..637e055 100644 --- a/strings/ui-strings.json +++ b/strings/ui-strings.json @@ -940,6 +940,14 @@ "platforms": ["android", "macos", "ios"], "specs": ["SET-02", "SET-01"] }, + { + "key": "common.open_bluetooth_settings", + "en": "Open Bluetooth Settings", + "vi": "Mở cài đặt Bluetooth", + "comment": "Button next to the Bluetooth-off status line, opens x-apple.systempreferences:com.apple.BluetoothSettings (SET-03 field 18)", + "platforms": ["macos"], + "specs": ["SET-03"] + }, { "key": "common.open_settings", "en": "Open Settings", @@ -1360,6 +1368,15 @@ "platforms": ["macos"], "specs": ["WEB-03"] }, + { + "key": "infoplist.bluetooth_always", + "plist_key": "NSBluetoothAlwaysUsageDescription", + "en": "HandLive connects to your Android phone over Bluetooth when they aren't on the same Wi-Fi network.", + "vi": "HandLive kết nối với điện thoại Android qua Bluetooth khi hai máy không cùng mạng Wi-Fi.", + "comment": "Purpose string of the Bluetooth permission, asked during setup (SET-03 field 17); shared with AUDIO-01, whose wording must cover call audio again when Phase 4 reopens", + "platforms": ["macos"], + "specs": ["SET-03"] + }, { "key": "infoplist.focus_status_usage", "plist_key": "NSFocusStatusUsageDescription", @@ -1921,6 +1938,30 @@ "platforms": ["android"], "specs": ["SET-01"] }, + { + "key": "permission.bluetooth_body", + "en": "When your phone and Mac aren't on the same Wi-Fi network, HandLive connects them over Bluetooth. Android calls this permission Nearby devices. HandLive doesn't use it to find your location.", + "vi": "Khi điện thoại và Mac không cùng mạng Wi-Fi, HandLive nối hai máy qua Bluetooth. Android gọi quyền này là Thiết bị ở gần. HandLive không dùng nó để biết vị trí của bạn.", + "comment": "Body of the Android PermissionPrimer before BLUETOOTH_ADVERTISE/BLUETOOTH_CONNECT (Nearby devices); title permission.bluetooth_title (SET-01 field 12)", + "platforms": ["android"], + "specs": ["SET-01"] + }, + { + "key": "permission.bluetooth_body_mac", + "en": "When your Mac and phone aren't on the same Wi-Fi network, HandLive connects them over Bluetooth. macOS asks for permission next.", + "vi": "Khi Mac và điện thoại không cùng mạng Wi-Fi, HandLive nối hai máy qua Bluetooth. Tiếp theo macOS sẽ hỏi quyền.", + "comment": "Body of the Mac primer before CBCentralManager triggers the system Bluetooth dialog; title permission.bluetooth_title (SET-03 field 17, step B1)", + "platforms": ["macos"], + "specs": ["SET-03"] + }, + { + "key": "permission.bluetooth_title", + "en": "Connect Without Wi-Fi", + "vi": "Kết nối không cần Wi-Fi", + "comment": "Title of the Bluetooth permission primer, shared by the Android PermissionPrimer (body permission.bluetooth_body, SET-01 field 12) and the Mac primer before the system dialog (body permission.bluetooth_body_mac, SET-03 field 17)", + "platforms": ["android", "macos"], + "specs": ["SET-01", "SET-03"] + }, { "key": "permission.calls_primer", "en": "To announce incoming calls and let you answer or decline them on your Mac, HandLive needs to read the phone state, the call log, and your contacts.", @@ -2210,6 +2251,14 @@ "platforms": ["android"], "specs": ["SET-02"] }, + { + "key": "settings.bluetooth_link", + "en": "Connect Without Wi-Fi", + "vi": "Kết nối không cần Wi-Fi", + "comment": "Feature card label for link.bluetooth in the feature list (SET-01 field 10): status ready/needs_permission/permanently_denied/off", + "platforms": ["android"], + "specs": ["SET-01"] + }, { "key": "settings.call_app_calls", "en": "Calls from Other Apps", @@ -3252,6 +3301,22 @@ "platforms": ["macos", "ios"], "specs": ["SMS-05"] }, + { + "key": "status.bluetooth_denied", + "en": "HandLive can't use Bluetooth, so it can't connect to the phone without Wi-Fi. Turn on HandLive in System Settings › Privacy & Security › Bluetooth.", + "vi": "HandLive không dùng được Bluetooth nên không kết nối được với điện thoại khi không có Wi-Fi. Bật HandLive trong Cài đặt hệ thống › Quyền riêng tư & Bảo mật › Bluetooth.", + "comment": "Mac Bluetooth-unavailable status line when access is denied or restricted (SET-03 E8); shown with common.open_system_settings", + "platforms": ["macos"], + "specs": ["SET-03", "CONN-05"] + }, + { + "key": "status.bluetooth_off", + "en": "Bluetooth is off. Turn it on to connect without Wi-Fi.", + "vi": "Bluetooth đang tắt. Bật Bluetooth để kết nối khi không có Wi-Fi.", + "comment": "Mac Bluetooth-unavailable status line, shown under the status line while there is no LAN session (SET-03 field 18); shown with common.open_bluetooth_settings", + "platforms": ["macos"], + "specs": ["SET-03", "CONN-05"] + }, { "key": "status.channel_internet", "en": "Internet", @@ -3276,6 +3341,23 @@ "platforms": ["android", "macos", "ios"], "specs": ["PAIR-02"] }, + { + "key": "status.connected_bluetooth", + "en": "Connected via Bluetooth", + "vi": "Đã kết nối qua Bluetooth", + "comment": "State Connected while the session goes over Bluetooth; new catalog key (S7.1)", + "platforms": ["android", "macos", "ios"], + "specs": ["0.11"] + }, + { + "key": "status.connected_bluetooth_to", + "en": "Connected via Bluetooth to {device_name}", + "vi": "Đã kết nối qua Bluetooth với {device_name}", + "comment": "Status line with the device's name (Mac menu bar menu); also the VoiceOver/TalkBack text of the indicator; new catalog key (S7.1)", + "platforms": ["android", "macos", "ios"], + "args": [{"name": "device_name", "type": "string"}], + "specs": ["0.11"] + }, { "key": "status.connected_internet", "en": "Connected over the internet", diff --git a/test-vectors/README.md b/test-vectors/README.md index b963e09..1aaf31a 100644 --- a/test-vectors/README.md +++ b/test-vectors/README.md @@ -94,6 +94,91 @@ The whole PAIR-01 exchange (API 1–5 and the body of API 8) and the PAIR-02 Sec ### `discovery-hint.json` TXT `h` hints (0.4.1, CONN-01 step 3 and API 1–2) from the `prk` of `pair-prk.json`. `kind` = `key` (one per pair of `pair-prk.json`): `pair_id`, `prk_source`, `prk`, `info`, `length`, `k_disc`; `hours` = `[{hour, message, mac, hint}]` with `message` = `"HLDISC1"` ‖ hour int64 BE, for hours −1, 0, 1, 2 and 479762–479765; `clock` = `[{now_ms, hour, advertised, accepted}]`: the hour, the hint the phone advertises and the hints a client at that time accepts (`[previous, current, next]`), at 0, 3 599 999, 3 600 000, 1 727 150 000 123, 1 727 150 399 999 and 1 727 150 400 000. `kind` = `match`: a phone with the pairs `phone_pairs` advertises `txt_h` (in that order) at `phone_now_ms` (`phone_hour`); a client of `client_pair` at `client_now_ms` (`client_hour`) accepts `accepted` and must find `matched_hint`; `matched_as` = `previous`, `current` or `next` is the phone's hour relative to the client's. Scenarios: the same hour; the phone's clock behind and ahead by 1 ms across the hour boundary; behind and ahead by exactly one hour. `invalid_vectors`: `{name, pair, reason, client_now_ms, client_hour, txt_h}` plus evidence; the client must **not** treat `txt_h` as a match: `hint_outside_window` (`phone_now_ms`, `phone_hour`: the phone's hour is two away from the client's — ahead by one hour and 1 ms across two boundaries, or behind by exactly two hours; 0.4.1 accepts only the previous, current and next hour), `wrong_byte_order` (hour as int64 LE), `wrong_encoding` (hour as int32 BE), `wrong_label` (`"HLDISC1|"`), `wrong_key` (HMAC keyed with `PRK` instead of `K_disc`, `mac_key`); `message` = the bytes that were MACed. +### `bt-pairing.json` +Bluetooth pairing keys and the pairing-link "first frame" (00-common-specs.md 0.4.5 "Keys", "Pairing mode", +"Link-security handshake"), chained from the `pairing_secret` of `pair-prk.json`. `kind` = `key` (one per pair): +`pair_id`, `pairing_secret_source`, `pairing_secret`, `k_bt_pdisc_info`, `k_bt_pdisc` = HKDF-SHA256(ikm = +`pairing_secret`, empty salt, info `"handlive/v1/bt-pair-disc"`, L 32), `k_bt_plink_info`, `k_bt_plink` (same with +info `"handlive/v1/bt-pair-link"`), `pr_bt_label` (`"HLBTPAIR1"`), `pr_bt_mac` = HMAC-SHA256(`k_bt_pdisc`, +`pr_bt_label`), `pr_bt` = its first 4 bytes. `kind` = `first_frame`: message 1 (tokens `"psk, e"`) of +`Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `psk` = `k_bt_plink` of pair 1, `mode` +`qr`, `prologue` = `"HLBT1"` ‖ `n_s`. Every intermediate value of the Noise symmetric state is recorded: `h0` +(`=InitializeSymmetric`), `h_after_prologue` (`MixHash`), `mix_key_and_hash` (`{temp_key, ck, temp_h, temp_k, h}` +of the `psk` token), `h_after_psk`, `ck_after_psk`, `k`, `e_priv`/`e_pub`, `h_after_e`, `nonce` (12 zero bytes, +`n = 0`), `ciphertext_tag` (`EncryptAndHash("")`, 16 bytes since the plaintext is empty), `h_final`, `message1` = +`e_pub` ‖ `ciphertext_tag`, and `record` = `rlen` (uint16 BE) ‖ `kind` (`0x02`) ‖ `message1` (0.4.5 "Link +records"). This is **not** a vector published by the Noise specification (it publishes none for HandLive's own +psk/prologue): it is HandLive's own use of that document's algorithm, so it needs its own review before A7.1a/M7.1b +rely on it. No PIN-mode vector yet (`psk` = `k_bt_pinlink`): `cpace.json` now has the `ISK`, this file just has not +been wired to it yet (left for a follow-up task). +`invalid_vectors`: `{name, reason, n_s, e_pub, ciphertext_tag}` — the responder's candidate psk must **not** open +the tag: `wrong_key` (tries `k_bt_plink` of the other pair, `wrong_psk_source`, `wrong_psk`), `tag_mismatch` (last +bit of `ciphertext_tag` flipped), `message_tampered` (a bit of `e_pub` flipped in transit), `aad_mismatch` (the +same `message1` replayed under a different `n_s`, so a different `prologue` — "the prologue binds `n_s`, so a +recorded `noise1` never verifies on a new channel"). + +### `bt-link.json` +`K_bt_disc`/`K_bt_link` (00-common-specs.md 0.4.5 "Keys"), chained from the `PRK` of `pair-prk.json` — unlike +`K_bt_pdisc`/`K_bt_plink` of `bt-pairing.json` (live only for one QR), these live for the life of the pair and +are used for an ordinary Bluetooth link to an already-paired phone. `kind` = `key`: `pair_id`, `prk_source`, +`prk`, `k_bt_disc_info`, `k_bt_disc`, `k_bt_link_info`, `k_bt_link`. `kind` = `hint`: the rotating advertising +hint (not the pairing one) = the first 4 bytes of HMAC-SHA256(`K_bt_disc`, `"HLBTDISC1"` ‖ epoch int64 BE), +`epoch` = `floor(now_ms / BT_HINT_PERIOD)`, shown at a few epochs. `kind` = `handshake`: the full link-security +handshake (0.4.5 "Link-security handshake" steps 1–4), `psk` = `k_bt_link`: `challenge_record` (kind `0x01`, +`n_s`), then message 1 (`"psk, e"`, `noise1_record`, kind `0x02`) and message 2 (`"e, ee"`, `noise2_record`, kind +`0x03`) of `Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `prologue` = `"HLBT1"` ‖ +`n_s`, every intermediate symmetric-state value recorded for both messages, then `Split()` into `c1` (C encrypts, +S decrypts) and `c2` (the other way) — the two CipherStates of steps 4–5. Not a vector published by the Noise +specification (see the note under `bt-pairing.json`); message 1 was reviewed in #3328, message 2/`Split()` are +new and need their own review. `invalid_vectors`: `wrong_key`, `tag_mismatch_noise1`, `tag_mismatch_noise2` (0.4.5 +step 4: "C verifies its payload tag — failure → C closes the channel, CONN-05 E3"), `replayed_channel` (0.4.5 +step 3: "The prologue binds `n_s`, so a recorded `noise1` never verifies on a new channel"). + +### `bt-frame.json` +Transport record framing after link-security step 4 (00-common-specs.md 0.4.5 "Link records", "Transport records +and channels"), encrypted with `c1` of `bt-link.json` (Noise transport message, nonce = 4 zero bytes ‖ a 64-bit +little-endian counter, empty AAD). `kind` = `transport_frame`: plaintext `chan` (`0x01` control, `0x02` bulk) ‖ +`flags` (bit0 `more`, bit1 `long`) ‖ `dlen` (uint16 BE) ‖ `data` ‖ zero padding up to 256/1,024/4,096 bytes, then +`ciphertext_tag` and `record` = `rlen` (uint16 BE, one of 272/1,040/4,112) ‖ `ciphertext_tag`. Covers a short +control fragment, a 2-fragment long-control message, and a bulk fragment. `kind` = `preauth_frame`: the +`challenge`/`reject` wire bytes (`rlen` ‖ kind ‖ body) as a packaging example. `invalid_vectors` match the five +named in S7.1a plus one more from "Link records": `rlen_out_of_bounds` (not 272/1,040/4,112 → close before +allocating more), `preauth_record_over_bound` (over `BT_PREAUTH_RECORD_MAX` = 64 bytes), `unknown_chan` (neither +`0x01` nor `0x02`), `more_without_long` (`more` = 1 on the short lane, `long` = 0), `bulk_fragment_over_max` (a +long-lane or bulk fragment's `dlen` over `BT_BULK_FRAGMENT_MAX` = 1,020), `message_over_max` (a long-lane +message's running total over `BT_MESSAGE_MAX` = 256 KiB before its last fragment). + +### `bt-advertisement.json` +The service-data bytes the app itself controls inside the scan response (00-common-specs.md 0.4.5 +"Advertisement"): `ver` (always `0x01`) ‖ `psm` (uint16 LE) ‖ `hints` (4 bytes each, 0…`BT_ADV_MAX_HINTS` = 2); +the OS adds the other 18 bytes of the AD structure (flags, 128-bit UUID) to reach the 21/25/29-byte scan response +total of the 0.4.5 table — out of scope here, only recorded as `scan_response_total` for cross-checking. `kind` = +`scan_response`: 0, 1 and 2 hints (chained from `bt-link.json`'s `K_bt_disc`). `kind` = `pin_window`: `hints` = +only `BT_PIN_HINT` (`0x00000000`), the same for every phone while a PIN pairing window is open (AC9). +`invalid_vectors`: `wrong_version` (`ver` ≠ `0x01` → the client ignores the whole advertisement), +`hint_outside_window` (a hint computed at an epoch outside the client's accepted `[e-4, e+4]` window → no match). + +### `cpace.json` +CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512), used for PIN pairing over Bluetooth with +no network (00-common-specs.md 0.4.5, PAIR-01 flow C). `kind` = `official_generator`: the draft's own +`calculate_generator` vector (Appendix B.1.1) — `H`, `PRS`, `CI`, `sid`, `generator_string`, +`hash_generator_string`, `decoded_field_element`, `g`. `kind` = `official_message`: the draft's own message +A/B, K and ISK vector (Appendix B.1.2–B.1.7) — `g`, `ya`/`ADa`/`Ya`, `yb`/`ADb`/`Yb`, `K`, +`ISK_initiator_responder` (the initiator/responder transcript HandLive uses, since C always initiates and S +always responds), plus `ISK_parallel_source` for reference (the draft's parallel/symmetric variant, not used by +HandLive). `kind` = `handlive_pin`: HandLive's own case — `pin` (UTF-8 bytes = PRS, one vector keeps a leading +zero), `ci` = `"HLBTPIN1"` fixed, `n_s` (32 bytes, doubles as `sid`), `ya`/`yb`, `g`, `Ya`, `Yb`, `K`, `ISK`, +`k_bt_pinlink_info`/`k_bt_pinlink` = HKDF-SHA256(ikm = `ISK`, salt = `n_s`, info `"handlive/v1/bt-pin-link"`, L +32), `k_bt_pin_info`/`k_bt_pin` (same, info `"handlive/v1/bt-pin-pair"`). `invalid_vectors`: +`low_order_ya`/`low_order_yb` — the other side receives a low-order point from the draft's Appendix B.1.10 (`u0`, +in the "MUST trigger abort" set regardless of scalar) as `Ya`/`Yb`; `K` must come back `None` (`G.I`) and the +channel closes without an answer (0.4.5: "An all-zero `K` → close without answer"). `sid_mismatch`: C and S use +different `n_s` (`n_s_c`/`n_s_s`) for the same PIN — `isk_c` and `isk_s` must differ. `wrong_pin`: one side +mistypes the PIN (`pin_c`/`pin_s`, same `n_s`) — the generators differ, so `isk_c` and `isk_s` must differ (or +either side aborts). Neither of the last two is an abort inside CPace itself; they only surface once `noise1` +(psk = `K_bt_pinlink`) fails to open in the next step — `bt-pairing.json` does not yet have a PIN-mode +`first_frame` vector wired to this `ISK`, left for a follow-up task. + ### `session-handshake.json` Chained from `pair-prk.json` by `pair_id`. `k_auth`; `client_eph_priv/pub`, `client_nonce` (32); `t1`, `hello_mac` = HMAC-SHA256(k_auth, t1); `server_eph_*`, `server_nonce`, `t2`, `welcome_mac`; `eph_shared` = X25519(eph C, eph S); `secret_salt` = SHA-256(t2); `secret` (64) = HKDF(eph_shared ‖ prk, secret_salt, `"handlive/v1/session"`, 64); `k_c2s` = secret[0:32], `k_s2c` = secret[32:64]. `hello_plaintext`/`welcome_plaintext`: JSON `{op, data}` with `eph`/`nonce`/`mac` as b64u; `hello_envelope`/`welcome_envelope`: envelope `type = session`, `payload` = b64 of the plaintext (not encrypted). `invalid_vectors`: `{key, message, mac}` — HMAC(key, message) compared in constant time with `mac` must **differ**. diff --git a/test-vectors/README.vi.md b/test-vectors/README.vi.md index 22d8234..761fece 100644 --- a/test-vectors/README.vi.md +++ b/test-vectors/README.vi.md @@ -94,6 +94,90 @@ Toàn bộ trao đổi PAIR-01 (API 1–5 và thân request của API 8) và Mã ### `discovery-hint.json` Gợi ý TXT `h` (0.4.1, CONN-01 bước 3 và API 1–2) từ `prk` của `pair-prk.json`. `kind` = `key` (mỗi cặp của `pair-prk.json` một vector): `pair_id`, `prk_source`, `prk`, `info`, `length`, `k_disc`; `hours` = `[{hour, message, mac, hint}]` với `message` = `"HLDISC1"` ‖ giờ int64 BE, cho các giờ −1, 0, 1, 2 và 479762–479765; `clock` = `[{now_ms, hour, advertised, accepted}]`: giờ, gợi ý điện thoại quảng bá và các gợi ý client chấp nhận vào lúc đó (`[giờ trước, giờ này, giờ sau]`), tại 0, 3 599 999, 3 600 000, 1 727 150 000 123, 1 727 150 399 999 và 1 727 150 400 000. `kind` = `match`: điện thoại có các cặp `phone_pairs` quảng bá `txt_h` (theo đúng thứ tự đó) lúc `phone_now_ms` (`phone_hour`); client của `client_pair` lúc `client_now_ms` (`client_hour`) chấp nhận `accepted` và phải tìm thấy `matched_hint`; `matched_as` = `previous`, `current` hoặc `next` là giờ của điện thoại so với giờ của client. Kịch bản: cùng giờ; đồng hồ điện thoại chậm hơn và nhanh hơn 1 ms qua mốc giờ; chậm hơn và nhanh hơn đúng một giờ. `invalid_vectors`: `{name, pair, reason, client_now_ms, client_hour, txt_h}` kèm bằng chứng; client **không** được coi `txt_h` là khớp: `hint_outside_window` (`phone_now_ms`, `phone_hour`: giờ của điện thoại cách giờ của client hai giờ — nhanh hơn một giờ và 1 ms vượt hai mốc giờ, hoặc chậm hơn đúng hai giờ; 0.4.1 chỉ chấp nhận giờ trước, giờ hiện tại và giờ sau), `wrong_byte_order` (giờ int64 LE), `wrong_encoding` (giờ int32 BE), `wrong_label` (`"HLDISC1|"`), `wrong_key` (HMAC khóa bằng `PRK` thay vì `K_disc`, `mac_key`); `message` = các byte đã được MAC. +### `bt-pairing.json` +Khóa ghép đôi qua Bluetooth và "khung đầu" (first frame) của liên kết ghép đôi (00-common-specs.md 0.4.5 "Keys", +"Pairing mode", "Link-security handshake"), nối từ `pairing_secret` của `pair-prk.json`. `kind` = `key` (mỗi cặp +một vector): `pair_id`, `pairing_secret_source`, `pairing_secret`, `k_bt_pdisc_info`, `k_bt_pdisc` = +HKDF-SHA256(ikm = `pairing_secret`, salt rỗng, info `"handlive/v1/bt-pair-disc"`, L 32), `k_bt_plink_info`, +`k_bt_plink` (tương tự, info `"handlive/v1/bt-pair-link"`), `pr_bt_label` (`"HLBTPAIR1"`), `pr_bt_mac` = +HMAC-SHA256(`k_bt_pdisc`, `pr_bt_label`), `pr_bt` = 4 byte đầu của nó. `kind` = `first_frame`: message 1 (token +`"psk, e"`) của `Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `psk` = `k_bt_plink` +của cặp 1, `mode` `qr`, `prologue` = `"HLBT1"` ‖ `n_s`. Mọi giá trị trung gian của trạng thái đối xứng Noise đều +được ghi lại: `h0` (`InitializeSymmetric`), `h_after_prologue` (`MixHash`), `mix_key_and_hash` +(`{temp_key, ck, temp_h, temp_k, h}` của token `psk`), `h_after_psk`, `ck_after_psk`, `k`, `e_priv`/`e_pub`, +`h_after_e`, `nonce` (12 byte 0, `n = 0`), `ciphertext_tag` (`EncryptAndHash("")`, 16 byte vì plaintext rỗng), +`h_final`, `message1` = `e_pub` ‖ `ciphertext_tag`, và `record` = `rlen` (uint16 BE) ‖ `kind` (`0x02`) ‖ +`message1` (0.4.5 "Link records"). Đây **không** phải vector do tài liệu Noise công bố (tài liệu đó không phát +hành vector cho psk/prologue riêng của HandLive): đây là cách HandLive dùng đúng thuật toán của tài liệu đó, nên +cần một vòng rà soát riêng trước khi A7.1a/M7.1b khoá theo. Chưa có vector mode PIN (`psk` = `k_bt_pinlink`): +`cpace.json` đã có `ISK`, chỉ là file này chưa nối vào (để lại cho một task sau). `invalid_vectors`: +`{name, reason, n_s, e_pub, ciphertext_tag}` — +psk ứng viên của S **không** được mở tag: `wrong_key` (thử `k_bt_plink` của cặp khác, `wrong_psk_source`, +`wrong_psk`), `tag_mismatch` (lật bit cuối của `ciphertext_tag`), `message_tampered` (lật 1 bit của `e_pub` trên +đường truyền), `aad_mismatch` (phát lại đúng `message1` dưới `n_s` khác, nên `prologue` khác — "prologue buộc +`n_s`, nên một `noise1` ghi lại không bao giờ xác thực trên kênh mới"). + +### `bt-link.json` +`K_bt_disc`/`K_bt_link` (00-common-specs.md 0.4.5 "Keys"), nối từ `PRK` của `pair-prk.json` — khác +`K_bt_pdisc`/`K_bt_plink` của `bt-pairing.json` (chỉ sống theo một QR), hai khóa này sống suốt đời cặp và dùng +cho liên kết Bluetooth bình thường tới một điện thoại đã ghép đôi. `kind` = `key`: `pair_id`, `prk_source`, +`prk`, `k_bt_disc_info`, `k_bt_disc`, `k_bt_link_info`, `k_bt_link`. `kind` = `hint`: hint quảng bá xoay vòng +(khác hint lúc ghép đôi) = 4 byte đầu của HMAC-SHA256(`K_bt_disc`, `"HLBTDISC1"` ‖ epoch int64 BE), `epoch` = +`floor(now_ms / BT_HINT_PERIOD)`, cho vài kỷ nguyên ví dụ. `kind` = `handshake`: bắt tay bảo mật liên kết đầy đủ +(0.4.5 "Link-security handshake" bước 1–4), `psk` = `k_bt_link`: `challenge_record` (kind `0x01`, `n_s`), rồi +message 1 (`"psk, e"`, `noise1_record`, kind `0x02`) và message 2 (`"e, ee"`, `noise2_record`, kind `0x03`) của +`Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `prologue` = `"HLBT1"` ‖ `n_s`, mọi +giá trị trung gian của trạng thái đối xứng được ghi lại cho cả hai message, rồi `Split()` ra `c1` (C mã hoá/S +giải mã) và `c2` (ngược lại) — hai CipherState của bước 4-5. Không phải vector do tài liệu Noise công bố (xem +ghi chú ở `bt-pairing.json`); message 1 đã được soát ở #3328, message 2/`Split()` là phần mới, cần soát riêng. +`invalid_vectors`: `wrong_key`, `tag_mismatch_noise1`, `tag_mismatch_noise2` (0.4.5 bước 4: "C verifies its +payload tag — failure → C closes the channel, CONN-05 E3"), `replayed_channel` (0.4.5 bước 3: "prologue buộc +`n_s`, nên một `noise1` ghi lại không bao giờ xác thực trên kênh mới"). + +### `bt-frame.json` +Đóng khung bản ghi transport sau bước 4 của bắt tay bảo mật liên kết (00-common-specs.md 0.4.5 "Link records", +"Transport records and channels"), mã hoá bằng `c1` của `bt-link.json` (Noise transport message, nonce = 4 byte +0 ‖ bộ đếm 64-bit little-endian, AAD rỗng). `kind` = `transport_frame`: bản rõ `chan` (`0x01` control, `0x02` +bulk) ‖ `flags` (bit0 `more`, bit1 `long`) ‖ `dlen` (uint16 BE) ‖ `data` ‖ đệm 0 tới 256/1.024/4.096 byte, rồi +`ciphertext_tag` và `record` = `rlen` (uint16 BE, một trong 272/1.040/4.112) ‖ `ciphertext_tag`. Có một mảnh làn +ngắn, một tin làn dài 2 mảnh, và một mảnh bulk. `kind` = `preauth_frame`: byte wire của `challenge`/`reject` +(`rlen` ‖ kind ‖ body) làm ví dụ đóng gói. `invalid_vectors` khớp đúng 5 luật nêu trong S7.1a cộng thêm 1 từ +"Link records": `rlen_out_of_bounds` (không thuộc 272/1.040/4.112 → đóng kênh trước khi cấp phát đọc thêm), +`preauth_record_over_bound` (vượt `BT_PREAUTH_RECORD_MAX` = 64 byte), `unknown_chan` (khác cả `0x01` và `0x02`), +`more_without_long` (`more` = 1 trên làn ngắn, `long` = 0), `bulk_fragment_over_max` (`dlen` một mảnh làn +dài/bulk vượt `BT_BULK_FRAGMENT_MAX` = 1.020), `message_over_max` (tổng byte một tin làn dài vượt +`BT_MESSAGE_MAX` = 256 KiB trước mảnh cuối). + +### `bt-advertisement.json` +Byte service data app tự kiểm soát trong scan response (00-common-specs.md 0.4.5 "Advertisement"): `ver` (luôn +`0x01`) ‖ `psm` (uint16 LE) ‖ `hints` (4 byte mỗi hint, 0…`BT_ADV_MAX_HINTS` = 2); hệ điều hành thêm 18 byte còn +lại của cấu trúc AD (cờ, UUID 128-bit) để ra tổng scan response 21/25/29 byte của bảng 0.4.5 — không thuộc +phạm vi ở đây, chỉ ghi `scan_response_total` để đối chiếu. `kind` = `scan_response`: 0, 1 và 2 hint (nối từ +`K_bt_disc` của `bt-link.json`). `kind` = `pin_window`: `hints` = chỉ `BT_PIN_HINT` (`0x00000000`), giống nhau +cho mọi điện thoại khi cửa sổ ghép đôi PIN đang mở (AC9). `invalid_vectors`: `wrong_version` (`ver` ≠ `0x01` → +client bỏ qua cả advertisement), `hint_outside_window` (hint tính ở kỷ nguyên ngoài cửa sổ chấp nhận `[e-4, e+4]` +của client → không khớp). + +### `cpace.json` +CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512), dùng cho ghép đôi bằng PIN qua +Bluetooth không mạng (00-common-specs.md 0.4.5, PAIR-01 luồng C). `kind` = `official_generator`: vector +`calculate_generator` do draft công bố (Appendix B.1.1) — `H`, `PRS`, `CI`, `sid`, `generator_string`, +`hash_generator_string`, `decoded_field_element`, `g`. `kind` = `official_message`: vector message A/B, K và ISK +do draft công bố (Appendix B.1.2–B.1.7) — `g`, `ya`/`ADa`/`Ya`, `yb`/`ADb`/`Yb`, `K`, `ISK_initiator_responder` +(biến thể khởi tạo/trả lời mà HandLive dùng, vì C luôn khởi tạo và S luôn trả lời), thêm `ISK_parallel_source` +để tham khảo (biến thể song hành của draft, HandLive không dùng). `kind` = `handlive_pin`: case riêng của +HandLive — `pin` (UTF-8 bytes = PRS, một vector giữ số 0 đứng đầu), `ci` = `"HLBTPIN1"` cố định, `n_s` (32 byte, +cũng là `sid`), `ya`/`yb`, `g`, `Ya`, `Yb`, `K`, `ISK`, `k_bt_pinlink_info`/`k_bt_pinlink` = +HKDF-SHA256(ikm = `ISK`, salt = `n_s`, info `"handlive/v1/bt-pin-link"`, L 32), `k_bt_pin_info`/`k_bt_pin` +(tương tự, info `"handlive/v1/bt-pin-pair"`). `invalid_vectors`: `low_order_ya`/`low_order_yb` — phía đối diện +nhận một điểm bậc thấp từ Appendix B.1.10 của draft (`u0`, nằm trong tập "PHẢI kích hoạt abort" bất kể vô +hướng) làm `Ya`/`Yb`; `K` phải trả về `None` (`G.I`) và kênh đóng không trả lời (0.4.5: "Một `K` toàn 0 → đóng +kênh không trả lời"). `sid_mismatch`: C và S dùng `n_s` khác nhau (`n_s_c`/`n_s_s`) cho cùng một PIN — `isk_c` +và `isk_s` phải khác nhau. `wrong_pin`: một phía gõ sai PIN (`pin_c`/`pin_s`, cùng `n_s`) — generator hai phía +khác nhau nên `isk_c` và `isk_s` phải khác nhau (hoặc một phía abort). Cả hai trường hợp sau không phải abort +ngay trong CPace; chúng chỉ lộ ra khi `noise1` (psk = `K_bt_pinlink`) không mở được ở bước tiếp theo — +`bt-pairing.json` chưa nối vector `first_frame` mode PIN vào `ISK` này, để lại cho một task sau. + ### `session-handshake.json` Nối từ `pair-prk.json` theo `pair_id`. `k_auth`; `client_eph_priv/pub`, `client_nonce` (32); `t1`, `hello_mac` = HMAC-SHA256(k_auth, t1); `server_eph_*`, `server_nonce`, `t2`, `welcome_mac`; `eph_shared` = X25519(eph C, eph S); `secret_salt` = SHA-256(t2); `secret` (64) = HKDF(eph_shared ‖ prk, secret_salt, `"handlive/v1/session"`, 64); `k_c2s` = secret[0:32], `k_s2c` = secret[32:64]. `hello_plaintext`/`welcome_plaintext`: JSON `{op, data}` với `eph`/`nonce`/`mac` dạng b64u; `hello_envelope`/`welcome_envelope`: envelope `type = session`, `payload` = b64 của plaintext (chưa mã hóa). `invalid_vectors`: `{key, message, mac}` — HMAC(key, message) so hằng thời gian với `mac` phải **khác**. diff --git a/test-vectors/bt-advertisement.json b/test-vectors/bt-advertisement.json new file mode 100644 index 0000000..dedb90a --- /dev/null +++ b/test-vectors/bt-advertisement.json @@ -0,0 +1,99 @@ +{ + "description": "Byte service data app tự kiểm soát trong scan response (0.4.5 \"Advertisement\"): `ver` (1, luôn 0x01) ‖ `psm` (2, uint16 LE, PSM L2CAP hiện tại) ‖ `hints` (4 × n, n = 0…BT_ADV_MAX_HINTS = 2, mỗi hint = 4 byte đầu của HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE)) — K_bt_disc/hint lấy từ bt-link.json. OS thêm 18 byte cấu trúc AD (cờ + UUID 128-bit) để ra tổng scan response (21/25/29 byte cho n=0/1/2) — không thuộc phạm vi vector này, chỉ ghi `scan_response_total` để đối chiếu bảng 0.4.5. `kind` = \"pin_window\": `hints` = chỉ `BT_PIN_HINT` (0x00000000), đánh dấu cửa sổ ghép đôi PIN đang mở, giống nhau cho mọi điện thoại (AC9). invalid_vectors: wrong_version (ver ≠ 0x01 → bỏ qua cả advertisement), hint_outside_window (hint tính ở kỷ nguyên ngoài [e-4, e+4] của client → không khớp).", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Advertisement\"; K_bt_disc của bt-link.json", + "vectors": [ + { + "name": "0 hint (không cặp nào có session nào, không có cửa sổ QR)", + "kind": "scan_response", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [], + "hint_pairs": [], + "service_data_bytes": "01a05b", + "service_data_len": 3, + "ad_uuid_struct_bytes": 18, + "scan_response_total": 21, + "note": "service_data_bytes = phần app tự kiểm soát; OS thêm 18 byte UUID/cờ nữa mới ra tổng 21 byte" + }, + { + "name": "1 hint (một cặp gần đây nhất)", + "kind": "scan_response", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [ + "be52c088" + ], + "hint_pairs": [ + "cặp 1" + ], + "service_data_bytes": "01a05bbe52c088", + "service_data_len": 7, + "ad_uuid_struct_bytes": 18, + "scan_response_total": 25, + "note": "service_data_bytes = phần app tự kiểm soát; OS thêm 18 byte UUID/cờ nữa mới ra tổng 25 byte" + }, + { + "name": "2 hint (BT_ADV_MAX_HINTS, hai cặp gần đây nhất)", + "kind": "scan_response", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [ + "be52c088", + "cadcf56f" + ], + "hint_pairs": [ + "cặp 1", + "cặp 2" + ], + "service_data_bytes": "01a05bbe52c088cadcf56f", + "service_data_len": 11, + "ad_uuid_struct_bytes": 18, + "scan_response_total": 29, + "note": "service_data_bytes = phần app tự kiểm soát; OS thêm 18 byte UUID/cờ nữa mới ra tổng 29 byte" + }, + { + "name": "cửa sổ PIN đang mở (BT_PIN_HINT)", + "kind": "pin_window", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [ + "00000000" + ], + "service_data_bytes": "01a05b00000000", + "service_data_len": 7, + "note": "BT_PIN_HINT giống nhau cho mọi điện thoại, không gắn với cặp hay thiết bị nào (AC9)" + } + ], + "invalid_vectors": [ + { + "name": "ver khác 0x01", + "reason": "wrong_version", + "service_data_bytes": "02a05bf7e5a436", + "expect": "ver = 0x02 ≠ 0x01 → client bỏ qua toàn bộ advertisement, không xét hint" + }, + { + "name": "hint ngoài cửa sổ e-4..e+4", + "reason": "hint_outside_window", + "client_epoch": 1000, + "accepted_epochs": [ + 996, + 997, + 998, + 999, + 1000, + 1001, + 1002, + 1003, + 1004 + ], + "advertised_epoch": 1005, + "advertised_hint": "b5e2508e", + "service_data_bytes": "01a05bb5e2508e", + "expect": "advertised_epoch = 1005 cách client_epoch 5 kỷ nguyên, ngoài [e-4, e+4] → client không thấy khớp hint nào" + } + ] +} diff --git a/test-vectors/bt-frame.json b/test-vectors/bt-frame.json new file mode 100644 index 0000000..9886d2b --- /dev/null +++ b/test-vectors/bt-frame.json @@ -0,0 +1,140 @@ +{ + "description": "Khung bản ghi pre-auth (`rlen` uint16 BE ‖ `rlen` byte, kind đầu tiên của thân — 0.4.5 \"Link records\") và khung control/bulk sau bước 4 (chan(1) ‖ flags(1, bit0 more/bit1 long) ‖ dlen(2 BE) ‖ data ‖ 0-padding tới 256/1 024/4 096 byte, mã hoá Noise transport = ChaCha20-Poly1305(key, nonce = 4 zero ‖ bộ đếm 64-bit LE, AAD rỗng) ‖ tag 16 byte — rlen hợp lệ duy nhất 272/1 040/4 112). `key_source` = `c1` của bt-link.json (hướng C→S của cùng bắt tay). invalid_vectors khớp đúng 6 luật cận của 0.4.5/S7.1a: rlen_out_of_bounds (rlen không thuộc {272,1040,4112} → đóng kênh trước khi cấp phát đọc thêm), preauth_record_over_bound (bản ghi pre-auth vượt BT_PREAUTH_RECORD_MAX = 64 byte), unknown_chan (chan khác 0x01/0x02), more_without_long (more=1 trên làn ngắn, long=0), bulk_fragment_over_max (dlen một mảnh làn dài/bulk vượt BT_BULK_FRAGMENT_MAX = 1 020), message_over_max (tổng byte một tin làn dài vượt BT_MESSAGE_MAX = 256 KiB trước mảnh cuối).", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Link records\", \"Transport records and channels\"; c1 của bt-link.json", + "vectors": [ + { + "name": "control / làn ngắn (long=0, 1 mảnh)", + "kind": "transport_frame", + "chan": "0x01", + "more": false, + "long": false, + "dlen": 40, + "data": "f1f7f408b0ce73fc47a3a5d91a793ffa5d4bd3a431c306e4a88f72189846bf015287781f95f97a29", + "plaintext": "01000028f1f7f408b0ce73fc47a3a5d91a793ffa5d4bd3a431c306e4a88f72189846bf015287781f95f97a290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "padded_size": 256, + "key_source": "c1 của bt-link.json", + "counter": 10, + "nonce": "000000000a00000000000000", + "ciphertext_tag": "8d9c911e76158fd5af78661ba00616ba763fd7c85ef6271ff920cd8f6b6120a30f82d7dbb76a0f37d98c7ff90678390ddd3e7b3ef8a09fa202062b2c1e235ab3524916e8b015be38093861d2cbc0aa4e6b97a6624473a72b8decfa5f792df043d4c8d508d3488a8c4e5681ea641600ce4f313e4f3441cb5e5946f564959ddd067c9e183c7745b8449c0e95d5159119353f248e11aa91d2d73934300004f4541e7f8ad983ac5f6186a161b4449a20c85ebba9958c1dcce211ad796c0419b2e9e3939c944754bd10b964f57cbe0c30476ef22ad107f6930e9dcea44028b0f4880938509ae333cfdc9cc20e2078833a8bcfc9763e001a207f40aa99f72e49bafa7090bc148d05a16b2d53cf377753406514", + "rlen": 272, + "record": "01108d9c911e76158fd5af78661ba00616ba763fd7c85ef6271ff920cd8f6b6120a30f82d7dbb76a0f37d98c7ff90678390ddd3e7b3ef8a09fa202062b2c1e235ab3524916e8b015be38093861d2cbc0aa4e6b97a6624473a72b8decfa5f792df043d4c8d508d3488a8c4e5681ea641600ce4f313e4f3441cb5e5946f564959ddd067c9e183c7745b8449c0e95d5159119353f248e11aa91d2d73934300004f4541e7f8ad983ac5f6186a161b4449a20c85ebba9958c1dcce211ad796c0419b2e9e3939c944754bd10b964f57cbe0c30476ef22ad107f6930e9dcea44028b0f4880938509ae333cfdc9cc20e2078833a8bcfc9763e001a207f40aa99f72e49bafa7090bc148d05a16b2d53cf377753406514" + }, + { + "name": "control / làn dài, mảnh 1/2 (more=1, long=1)", + "kind": "transport_frame", + "chan": "0x01", + "more": true, + "long": true, + "dlen": 1020, + "data": "552379c089d30cc8d6a995c5ab698a2becf18714ffa34589c764451f644b7711cbfd9b8d5c44ba79950fcb552682884b2859190e34918edc3495f0097ef90f91e34223b66e3279e64bcd6aba89c922d440bfc5159dd56d8f8b9f6de01f7370f84ad4eec8c367cbfe2b74891c0b4a0392a0bc466132c699eb877a00e3d649cd9a9c1ea8480ead2776a4bcdb7073d8128d4a3f7a29ae5a67dce2e9017ff5cea9291d376c58ba892a990286517ec730fba72361beaca168cc1a48864a7e954110c77c0fb1d3b7de690b8a5fa5db0520521f1aa29859b0e4f4adffd3479f2c744e0406f98d620797998a55c4e5940b501626515e6c7468a2d698d27a9794f0215d3cbfcd950cef4047b27526d2ab28fd36fa70bdf122bce895cb640e6879745c2fab7bc56539dd1addb6b8a74ff9fb808cff338d4eb83651b1a95f42e5387ff542126b55f6f53cba990c090f3d78eb6829e9b8da138419b39d4ba42bbd4d9ad846f126d987f7c64e25ec27915723b1e74caa9fd66e79b78bf84788987fd09b229a04397c085dae4ffdbbca02ec9430d5c3e77db5bcc9dff1bda2462aa12b1c3678fe443c99648b97d0189312d9e305e1b98119a23531525f05a91613139f631e8b14a54561bc209eb233f06ca35b41bf7ea3dc2a590ac6db75361501f2fbb92c2c63c20c7bf384e4b05b8d73bfc345ef1f5735e8e409d7882ca4fbdcd39c866284bd1fa8cf6ebb7ce98df2a346e987a173d0d560740a8d3fcab2db6ef0c49434c76ba0b81ac0233fcc9eaa9d8c104de597d895de2d05b5be1ffff217c9b12d2fbf2076644b72f7d3b49b3681c13197d163b8bde2cef93238d72d16005d26290c457369ea7ebbf11c90c63b4d19150d37a78a37ff31aeb4701294ebc8c580f2014d50fa359a5a284b4b88684d92707c088dd9702da585fcf5bcf42fc99e5fd5785a36502c25ab75fa9c7fee7227687c7ceb88390606a57a86867e9d4ce8b743a0065e02b0e999f28d66c8f05a5d8109f40d4624275626686d066f7ecbeca646e07764d74cd962e19d534ac30968a235c3bf3304d163cd1f2df1f124279573fda31b8a558233576458e02f73d822df8aad9900794c9715f90a93a2ab691cc6c10c13989e937022a56e3e9341033b565c2a6c721b0d178e046f5c5b68c04467ae5efa9398a8fbaac6b76322b2d802bc80f5cd7a6cdcd9781bd66488ab259b6afe13290ad6e5b279ada9cdc85695ca6c41ae88f6d30b943ec1e6a3c94a4f9f7330f5b85046bb698095f3e41b906eadcf44ce620edbf7f326650c7e6293cb544660cace47004bdb1d32e2c91e55e5f45e2d7fdda0e854531fdb4e9c4f16f3f9574e6547b275be235b68464c69695798566bc0307f6884248baa69ded861d48566b8fc07df062a89c9bef0c659fa7a49f27949afe005a98220bd1490439c6419c2", + "plaintext": "010303fc552379c089d30cc8d6a995c5ab698a2becf18714ffa34589c764451f644b7711cbfd9b8d5c44ba79950fcb552682884b2859190e34918edc3495f0097ef90f91e34223b66e3279e64bcd6aba89c922d440bfc5159dd56d8f8b9f6de01f7370f84ad4eec8c367cbfe2b74891c0b4a0392a0bc466132c699eb877a00e3d649cd9a9c1ea8480ead2776a4bcdb7073d8128d4a3f7a29ae5a67dce2e9017ff5cea9291d376c58ba892a990286517ec730fba72361beaca168cc1a48864a7e954110c77c0fb1d3b7de690b8a5fa5db0520521f1aa29859b0e4f4adffd3479f2c744e0406f98d620797998a55c4e5940b501626515e6c7468a2d698d27a9794f0215d3cbfcd950cef4047b27526d2ab28fd36fa70bdf122bce895cb640e6879745c2fab7bc56539dd1addb6b8a74ff9fb808cff338d4eb83651b1a95f42e5387ff542126b55f6f53cba990c090f3d78eb6829e9b8da138419b39d4ba42bbd4d9ad846f126d987f7c64e25ec27915723b1e74caa9fd66e79b78bf84788987fd09b229a04397c085dae4ffdbbca02ec9430d5c3e77db5bcc9dff1bda2462aa12b1c3678fe443c99648b97d0189312d9e305e1b98119a23531525f05a91613139f631e8b14a54561bc209eb233f06ca35b41bf7ea3dc2a590ac6db75361501f2fbb92c2c63c20c7bf384e4b05b8d73bfc345ef1f5735e8e409d7882ca4fbdcd39c866284bd1fa8cf6ebb7ce98df2a346e987a173d0d560740a8d3fcab2db6ef0c49434c76ba0b81ac0233fcc9eaa9d8c104de597d895de2d05b5be1ffff217c9b12d2fbf2076644b72f7d3b49b3681c13197d163b8bde2cef93238d72d16005d26290c457369ea7ebbf11c90c63b4d19150d37a78a37ff31aeb4701294ebc8c580f2014d50fa359a5a284b4b88684d92707c088dd9702da585fcf5bcf42fc99e5fd5785a36502c25ab75fa9c7fee7227687c7ceb88390606a57a86867e9d4ce8b743a0065e02b0e999f28d66c8f05a5d8109f40d4624275626686d066f7ecbeca646e07764d74cd962e19d534ac30968a235c3bf3304d163cd1f2df1f124279573fda31b8a558233576458e02f73d822df8aad9900794c9715f90a93a2ab691cc6c10c13989e937022a56e3e9341033b565c2a6c721b0d178e046f5c5b68c04467ae5efa9398a8fbaac6b76322b2d802bc80f5cd7a6cdcd9781bd66488ab259b6afe13290ad6e5b279ada9cdc85695ca6c41ae88f6d30b943ec1e6a3c94a4f9f7330f5b85046bb698095f3e41b906eadcf44ce620edbf7f326650c7e6293cb544660cace47004bdb1d32e2c91e55e5f45e2d7fdda0e854531fdb4e9c4f16f3f9574e6547b275be235b68464c69695798566bc0307f6884248baa69ded861d48566b8fc07df062a89c9bef0c659fa7a49f27949afe005a98220bd1490439c6419c2", + "padded_size": 1024, + "key_source": "c1 của bt-link.json", + "counter": 11, + "nonce": "000000000b00000000000000", + "ciphertext_tag": "f54ba3fd3319ccee592fee4cbb05a96826b0352a626a1801e49b527ee052ef868a0c2fd13cfc5eec9e82ae65e3ee4b3eabc02a5751ab6c970c41be6fe1cdc46333d2e79b32e8b393c9e79426d5d5e8407e69be3c15b6ef1f8c71de9bc32279032c05b10be57cace7ad22d646ead726d76c964afc11ef6ebb58542128ba7a5a867748846cddeb7e47a136c5d5fee5e24ad85fd74670513c3e4ed687b7860a0df2d90eb4b8e82c030d913202fb17897ae978e02e6ffa945b1429013396137406c0bc12b69856c098e45a872078a31982e93068d0c07d03c77ae890b019e54d672dab8f7f646da9a2827c73321abd6dfa752fc2f775c35d92e0b05d8966182e4fdd5755fa46b891b3eeb51e7025833ee2509e35f1f0e8fc9f14550e9af8ef0c137909654acb35414f74ebdb625771a17238dcb55b9059e58bfd5c1408764f8a6c39a76c685139a9e0923e2761ed5cf068cf78519bc772231427273d33f6a39118c4b36844349184b4e4f343a6ca2419b1af6808d3c594b018fc064f0bc3227220d88e29ba08fded497a966cbc1a647f109730f8b1ec38ccede567928981ef1aed463d1409c743741cf8d269cf9359a4cb10de43ecfb72fe37e81f10f761d6852b785d3f86b93d5457660c5bc5be3cd7c533be4a57ca175c32497f292422d17b7b805f9b4d687c0497d11c15c738ecb61e5db234c365974455c1fe489b95bebc123b1e096149f5d1e4e055ebc6325da0d78dc1d0d93f6b53954c60d80aa740e6c6915153efef99d6090feb8fe69f28c041c7b137822648c5d636f5e81fb931150aa5d611a0c3ed1cf6a02fed4f43c14f2ec71e4284c54940eb9ff5e7f70655b7a6e422977f7590995857fcfc64b5bc5e60532a148c29816d363831bacfa8560a02b647682402f3f553564751c09d1cae6ac45d13e4e67f1ba21e1fd1ce0319907e9786c9d449378d89b63b7cf8619e44fb06586598a5073e2eb6971a48e4f010b49321edd9654c3c6893f432b09e160f3d0a349410b273f4a00fbb194f60626a8e7baf9763fa1dd489731a4533a5405c58a2425515671331256843f8a7d4966d5e4470fc9a6fc164d6abcf22dd603582101a5c9850c2a70a8abfec5a2cd115b46d12bcef0fab26e15babd72b15b39240b7c03f4773242d113526b17019d3bfa46dc1eea82e6d6292e6a8732bff5414789507abbdc0e50f4f4e28ce094262e9ae6b8b9f3a3f776c831ded9f4c0616123ad8567b83f22c796bec86585c925332eee53a0df9406512996e0c9a2ce69abb85f819380239396e07685773bf08e5725a21416034f9dd71c02288bce287aa522f7ae0026f1f16d3a71546b9d55880d26eab14a335bc15ce16b895d945ba2edec528912835df7cad961a32e636467cc8e2554e5255c7717e146b827f0b47193d3f43bd0b735910ca3a7a509394a3527151de397afae8d2b669d4fbc228ecb7e3a67d47", + "rlen": 1040, + "record": "0410f54ba3fd3319ccee592fee4cbb05a96826b0352a626a1801e49b527ee052ef868a0c2fd13cfc5eec9e82ae65e3ee4b3eabc02a5751ab6c970c41be6fe1cdc46333d2e79b32e8b393c9e79426d5d5e8407e69be3c15b6ef1f8c71de9bc32279032c05b10be57cace7ad22d646ead726d76c964afc11ef6ebb58542128ba7a5a867748846cddeb7e47a136c5d5fee5e24ad85fd74670513c3e4ed687b7860a0df2d90eb4b8e82c030d913202fb17897ae978e02e6ffa945b1429013396137406c0bc12b69856c098e45a872078a31982e93068d0c07d03c77ae890b019e54d672dab8f7f646da9a2827c73321abd6dfa752fc2f775c35d92e0b05d8966182e4fdd5755fa46b891b3eeb51e7025833ee2509e35f1f0e8fc9f14550e9af8ef0c137909654acb35414f74ebdb625771a17238dcb55b9059e58bfd5c1408764f8a6c39a76c685139a9e0923e2761ed5cf068cf78519bc772231427273d33f6a39118c4b36844349184b4e4f343a6ca2419b1af6808d3c594b018fc064f0bc3227220d88e29ba08fded497a966cbc1a647f109730f8b1ec38ccede567928981ef1aed463d1409c743741cf8d269cf9359a4cb10de43ecfb72fe37e81f10f761d6852b785d3f86b93d5457660c5bc5be3cd7c533be4a57ca175c32497f292422d17b7b805f9b4d687c0497d11c15c738ecb61e5db234c365974455c1fe489b95bebc123b1e096149f5d1e4e055ebc6325da0d78dc1d0d93f6b53954c60d80aa740e6c6915153efef99d6090feb8fe69f28c041c7b137822648c5d636f5e81fb931150aa5d611a0c3ed1cf6a02fed4f43c14f2ec71e4284c54940eb9ff5e7f70655b7a6e422977f7590995857fcfc64b5bc5e60532a148c29816d363831bacfa8560a02b647682402f3f553564751c09d1cae6ac45d13e4e67f1ba21e1fd1ce0319907e9786c9d449378d89b63b7cf8619e44fb06586598a5073e2eb6971a48e4f010b49321edd9654c3c6893f432b09e160f3d0a349410b273f4a00fbb194f60626a8e7baf9763fa1dd489731a4533a5405c58a2425515671331256843f8a7d4966d5e4470fc9a6fc164d6abcf22dd603582101a5c9850c2a70a8abfec5a2cd115b46d12bcef0fab26e15babd72b15b39240b7c03f4773242d113526b17019d3bfa46dc1eea82e6d6292e6a8732bff5414789507abbdc0e50f4f4e28ce094262e9ae6b8b9f3a3f776c831ded9f4c0616123ad8567b83f22c796bec86585c925332eee53a0df9406512996e0c9a2ce69abb85f819380239396e07685773bf08e5725a21416034f9dd71c02288bce287aa522f7ae0026f1f16d3a71546b9d55880d26eab14a335bc15ce16b895d945ba2edec528912835df7cad961a32e636467cc8e2554e5255c7717e146b827f0b47193d3f43bd0b735910ca3a7a509394a3527151de397afae8d2b669d4fbc228ecb7e3a67d47" + }, + { + "name": "control / làn dài, mảnh 2/2 cuối (more=0, long=1)", + "kind": "transport_frame", + "chan": "0x01", + "more": false, + "long": true, + "dlen": 400, + "data": "c0caf6011520e0657f2538361174f12b934433e8a8a42cbd0292c9d6bb99abfe545506e13c351c8dafd2f2d9fe7aace1ef6b22fab7e04c2008c48b1a2faefa5d99d5207988187105be02557e7d304cc7484cb2d65d49d1fc6f4406003cd9faa7f0f59f096881ea621ec9eb1f428c599bab9634c335773018f476d1ea6d24c246eb1d6f07b83b3efdbabca652d1ab5d61d9b38ab2967b407aeeb26aa15c4d7751d446fe5e5d0e30c7dd0ef42793200a143c9cd5099f30f89cdd1d0c0ea73117e5a5e89a95218f242ea439c8e4646e19af0a7eb3be267dfd8b2cd4f1b5abf2a5415efee29e91a91149fcb3f1744735c5c22ca1a9b8cbf25280da15b00bb386a55fbfdd6c4d08e2d5a8227748c7ee63289d85e8d728f9e5db46bf339627162097b6d6902334ed329c5046ed433b4f80f286dcb7628bfdd1a9ed58db80edfe5734987bd73d08531f90d55e476ca6a8c0337d30b664d83ab49c4cf4ef84b6564594ecd19d3abdbb1b3126a457b99b322376f0273ddbfb6500b5f415a31d1eed75d517e2899ef3c94bea69e12c3fcfd1a04c42", + "plaintext": "01020190c0caf6011520e0657f2538361174f12b934433e8a8a42cbd0292c9d6bb99abfe545506e13c351c8dafd2f2d9fe7aace1ef6b22fab7e04c2008c48b1a2faefa5d99d5207988187105be02557e7d304cc7484cb2d65d49d1fc6f4406003cd9faa7f0f59f096881ea621ec9eb1f428c599bab9634c335773018f476d1ea6d24c246eb1d6f07b83b3efdbabca652d1ab5d61d9b38ab2967b407aeeb26aa15c4d7751d446fe5e5d0e30c7dd0ef42793200a143c9cd5099f30f89cdd1d0c0ea73117e5a5e89a95218f242ea439c8e4646e19af0a7eb3be267dfd8b2cd4f1b5abf2a5415efee29e91a91149fcb3f1744735c5c22ca1a9b8cbf25280da15b00bb386a55fbfdd6c4d08e2d5a8227748c7ee63289d85e8d728f9e5db46bf339627162097b6d6902334ed329c5046ed433b4f80f286dcb7628bfdd1a9ed58db80edfe5734987bd73d08531f90d55e476ca6a8c0337d30b664d83ab49c4cf4ef84b6564594ecd19d3abdbb1b3126a457b99b322376f0273ddbfb6500b5f415a31d1eed75d517e2899ef3c94bea69e12c3fcfd1a04c420000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "padded_size": 1024, + "key_source": "c1 của bt-link.json", + "counter": 12, + "nonce": "000000000c00000000000000", + "ciphertext_tag": "c7bd07c88a7e4fd888310a16cb2bd15e91cdf19b5e04a59d3e36ea65ed83ef33c7267c695275bf21dfc66aa9478c4235708040291cb766486c3b3d180303cf2eb99f495545b770034aa43a69f6816bd5bc66c3fa70605af54c0c793308f3b4454fe288614a50faa4ce82620d507a595a39ac0e063de5958dfb0e9aa0d00f126aa618683f0c7bd5ad10a403a62644f9e90b4b9451d0dca48b70d4e05bc2ec85cacb14261d9ca4eed0052566f529a660555f14abe5724e39417c1da446ba91dd480111b069331b13a08b4e413ae81cc974f5c511c5534a4b9820cdf054e2997ba9292290667498a6bd7dc2fb2f343727bb189f7b4c479f36766dda9370e1af0bd908a18366f090924f4b8afed303e0998d63e06ffdda125a95a0cd1d03d2baffb12a685f27bb4c6826f830f9feb98cdc727cf4014e7b37fa8f1c475bc4c5e9e708364eca88e8ea3f8933e042887c0630fc0dc5355f5496f618b052325444a108b30cf242aac92488ecdf7613f7ae84ecb2a0849eabc4c86fa6290fa7ea5ce2bf74296ec9416ad38e906d717ee3f760d28c6824d3d42e26964597930d53a19b9ac632bade37fdd13acbfdb277ce44e0bc45ca347ad440b5b2fe01cfa812c251e59fd6229a79a19a27a74bce0494fbe567abaa78785815efc672df551a3ecd7ff68de6e65ab2c89c6d56ada62fc5e9b137ab7b2071b28fc5ba7aa46a4f1a42da573769dd3f717bdd619396ff9d9982d3b1265ef8c9c69e3ec9e7a97301b2a5edee231c404c860e93e782ed3a093459db8ba59aadd5a53f6726b5e1e0d16001166d8b51e1bba3885183e88ce68ee11ac4aca30d78395e0ac00d82f8bc5e9cb4752d288e0e88e187e3ae835ee6b4d8c283986f59baee118e05648247712d3bc7a6f40e671ded5c0c2d83a93018e80ff6c700a76171b31012539b7b04ffdc24787f1339833f614e86604cc67076e384787a69a23afa612fed4e30bda1787062b1a1aad5a8a3969dd0d5fb4b1f4309a98104cbb5440b2cc0fb537d12411785e2180e3b626f9304dcc9c34c0f1826d8568ade6153bcc87bb30844bc720e130f90c6e9110d789cbf583c019038d9bf6654b794b9c1bdc5a9b02a227572026de18e5dac18b61f958654f165d9ee71c06e0201d82d92d88e82b5958706040c852b0c3cd7a4632f78d7c07513942989950d0dfa189af1b060e5a06336b534fe397aaede2ab4fbffad2f6317e1ef977219a1e89a88064e670bedc6f7c08aa4d564d96397ce6cc89e15f42ea4835bb45259f56540c835f56565c34b8f3a7baeeb6df91a7ed10cf53c8d900cd83d119512bc5d76ad2e8a3911990dbc07e7833f0c57794b0e7ad76405f6ec78db1b2ec603af792940054b39469fa909b50f62c970924418634352462908364214b42a7029094189ba3258dbadceb5a8316ef194a7f0749c6fac07afa3bd0143d8beac33cdccc2f811214b51", + "rlen": 1040, + "record": "0410c7bd07c88a7e4fd888310a16cb2bd15e91cdf19b5e04a59d3e36ea65ed83ef33c7267c695275bf21dfc66aa9478c4235708040291cb766486c3b3d180303cf2eb99f495545b770034aa43a69f6816bd5bc66c3fa70605af54c0c793308f3b4454fe288614a50faa4ce82620d507a595a39ac0e063de5958dfb0e9aa0d00f126aa618683f0c7bd5ad10a403a62644f9e90b4b9451d0dca48b70d4e05bc2ec85cacb14261d9ca4eed0052566f529a660555f14abe5724e39417c1da446ba91dd480111b069331b13a08b4e413ae81cc974f5c511c5534a4b9820cdf054e2997ba9292290667498a6bd7dc2fb2f343727bb189f7b4c479f36766dda9370e1af0bd908a18366f090924f4b8afed303e0998d63e06ffdda125a95a0cd1d03d2baffb12a685f27bb4c6826f830f9feb98cdc727cf4014e7b37fa8f1c475bc4c5e9e708364eca88e8ea3f8933e042887c0630fc0dc5355f5496f618b052325444a108b30cf242aac92488ecdf7613f7ae84ecb2a0849eabc4c86fa6290fa7ea5ce2bf74296ec9416ad38e906d717ee3f760d28c6824d3d42e26964597930d53a19b9ac632bade37fdd13acbfdb277ce44e0bc45ca347ad440b5b2fe01cfa812c251e59fd6229a79a19a27a74bce0494fbe567abaa78785815efc672df551a3ecd7ff68de6e65ab2c89c6d56ada62fc5e9b137ab7b2071b28fc5ba7aa46a4f1a42da573769dd3f717bdd619396ff9d9982d3b1265ef8c9c69e3ec9e7a97301b2a5edee231c404c860e93e782ed3a093459db8ba59aadd5a53f6726b5e1e0d16001166d8b51e1bba3885183e88ce68ee11ac4aca30d78395e0ac00d82f8bc5e9cb4752d288e0e88e187e3ae835ee6b4d8c283986f59baee118e05648247712d3bc7a6f40e671ded5c0c2d83a93018e80ff6c700a76171b31012539b7b04ffdc24787f1339833f614e86604cc67076e384787a69a23afa612fed4e30bda1787062b1a1aad5a8a3969dd0d5fb4b1f4309a98104cbb5440b2cc0fb537d12411785e2180e3b626f9304dcc9c34c0f1826d8568ade6153bcc87bb30844bc720e130f90c6e9110d789cbf583c019038d9bf6654b794b9c1bdc5a9b02a227572026de18e5dac18b61f958654f165d9ee71c06e0201d82d92d88e82b5958706040c852b0c3cd7a4632f78d7c07513942989950d0dfa189af1b060e5a06336b534fe397aaede2ab4fbffad2f6317e1ef977219a1e89a88064e670bedc6f7c08aa4d564d96397ce6cc89e15f42ea4835bb45259f56540c835f56565c34b8f3a7baeeb6df91a7ed10cf53c8d900cd83d119512bc5d76ad2e8a3911990dbc07e7833f0c57794b0e7ad76405f6ec78db1b2ec603af792940054b39469fa909b50f62c970924418634352462908364214b42a7029094189ba3258dbadceb5a8316ef194a7f0749c6fac07afa3bd0143d8beac33cdccc2f811214b51" + }, + { + "name": "bulk / một mảnh ảnh (more=1)", + "kind": "transport_frame", + "chan": "0x02", + "more": true, + "long": false, + "dlen": 1020, + "data": "f22ad204961b3d30a7b0c4657e279c9a91e252c4ab2eff7af7352de55021304584afa6e1456d309fdbfc3e635aa832d497de47608c0d0d833168c48e50ff6e98711cf30acb1770c9b43222a5fe5531df4b749e4ecf42097161347ee524b0869049fa419e07b28dac72197305451cc1bf4ef640fca34676cd8d8b37222a956b1b0c1b62c6a6d3ae18a71eb19091fdc68e10827e7b05433408897c69542b5e89b8b04880890b9539acf6df90640fb0a5ecb958a8b58afdd0057a92df36aa1802724d3c477a0ea37381c79f7a17f3c890ce0465beb72bbbba8140a3f7f43aca12f051020671297eebe457046a8cd25cab84114749619d83429c9e6c69804833023e9f7494ad927f3a6796a03d242e125e8b231a745c74d6e4a651fc6398d2c707b00294df69fb7543198023304b29b2a01f00ea25f7315d474e2db3b243aa0f9bec3db1021f649aafb598a5c28ae345564e101d659a34fa40fc799506de140990b8390cb5c041a8663dc7bcfe7d56fc513e58c3dde45875267ba769ad55c6183a2ab97317d625167b333cf9ed9d406ce50b5ec8cba84fde6ca86cdaa514694cf6167ebe3f4e1a5d4193ab97c451bbbe11bb144255568cd830945e3c6d45dde3ed4c6026ad9ace52087be7659d4a03d1d10fd86c85be8a4dee289f459167f8cadbbf9e277b41208435e10f5fabf4f44aea1aad05a35492e8d3657feeaa05d59cae35da2af9a92fba41967788a65bf83f62b0a270728c015e26642a4feaa04b9e7baa7cc436acf684fb0c481d5a6b0dd4c18d9b85cb9c8fae330a7398a7d897e66fae6d5aaab40a18b50e221811bba7dff4ea22a053031d939ad4d2e33ef0b588de735f0c21fd9ef9b71fdbf8fb03793e715a6f014d7f9a4df6cdafbe07c5f3fc1c8d9862c8f03ef0366293078a11a64cea35ddf29ebde98c510d38082d775fb41bcbaa445121ee1163d953225c60df049c5171e360908490c5c13f5cd789dc0b115a63cd8b2cd6701ace3722ab0588939d58b5b0c577195045c7c89f2ebd3600b73b22d733864385cd4a5e60bf0c73f06b5080ac348f8867df566a98e32a98805f4c8c64f86073cf3ffdfbaec4e6b4d36402bb92090ecec41b843a290d8711771b2925d7252bed0c0887341b37d87f8a7a1b42635bea3e4d5b6ccccec60d7b48330d94318a5fd0fc55d1f5f53838ac20180d20322a4f53d7b0ee9276eca7396247022556b7cb25f44bdb6254ee8428b7d608981c1da5896073cb4b0adfb65843c7a7cd5f77ab4d7a0bd3b38d681de7f496c46c27309ad7e4c4e004ee57a534ca689256fa79090c208c3b6e7a1f21dda9394b0be5b030b238f74fe74ddaf10e1d418fb5b08d8bf62b266a7ceff31c9d648b34957f9bfa13995d91032d115cdff2a4a03834bb5f07a75ae606541bfd5ad8f51868fe824da7b8d6f06288a82d", + "plaintext": "020103fcf22ad204961b3d30a7b0c4657e279c9a91e252c4ab2eff7af7352de55021304584afa6e1456d309fdbfc3e635aa832d497de47608c0d0d833168c48e50ff6e98711cf30acb1770c9b43222a5fe5531df4b749e4ecf42097161347ee524b0869049fa419e07b28dac72197305451cc1bf4ef640fca34676cd8d8b37222a956b1b0c1b62c6a6d3ae18a71eb19091fdc68e10827e7b05433408897c69542b5e89b8b04880890b9539acf6df90640fb0a5ecb958a8b58afdd0057a92df36aa1802724d3c477a0ea37381c79f7a17f3c890ce0465beb72bbbba8140a3f7f43aca12f051020671297eebe457046a8cd25cab84114749619d83429c9e6c69804833023e9f7494ad927f3a6796a03d242e125e8b231a745c74d6e4a651fc6398d2c707b00294df69fb7543198023304b29b2a01f00ea25f7315d474e2db3b243aa0f9bec3db1021f649aafb598a5c28ae345564e101d659a34fa40fc799506de140990b8390cb5c041a8663dc7bcfe7d56fc513e58c3dde45875267ba769ad55c6183a2ab97317d625167b333cf9ed9d406ce50b5ec8cba84fde6ca86cdaa514694cf6167ebe3f4e1a5d4193ab97c451bbbe11bb144255568cd830945e3c6d45dde3ed4c6026ad9ace52087be7659d4a03d1d10fd86c85be8a4dee289f459167f8cadbbf9e277b41208435e10f5fabf4f44aea1aad05a35492e8d3657feeaa05d59cae35da2af9a92fba41967788a65bf83f62b0a270728c015e26642a4feaa04b9e7baa7cc436acf684fb0c481d5a6b0dd4c18d9b85cb9c8fae330a7398a7d897e66fae6d5aaab40a18b50e221811bba7dff4ea22a053031d939ad4d2e33ef0b588de735f0c21fd9ef9b71fdbf8fb03793e715a6f014d7f9a4df6cdafbe07c5f3fc1c8d9862c8f03ef0366293078a11a64cea35ddf29ebde98c510d38082d775fb41bcbaa445121ee1163d953225c60df049c5171e360908490c5c13f5cd789dc0b115a63cd8b2cd6701ace3722ab0588939d58b5b0c577195045c7c89f2ebd3600b73b22d733864385cd4a5e60bf0c73f06b5080ac348f8867df566a98e32a98805f4c8c64f86073cf3ffdfbaec4e6b4d36402bb92090ecec41b843a290d8711771b2925d7252bed0c0887341b37d87f8a7a1b42635bea3e4d5b6ccccec60d7b48330d94318a5fd0fc55d1f5f53838ac20180d20322a4f53d7b0ee9276eca7396247022556b7cb25f44bdb6254ee8428b7d608981c1da5896073cb4b0adfb65843c7a7cd5f77ab4d7a0bd3b38d681de7f496c46c27309ad7e4c4e004ee57a534ca689256fa79090c208c3b6e7a1f21dda9394b0be5b030b238f74fe74ddaf10e1d418fb5b08d8bf62b266a7ceff31c9d648b34957f9bfa13995d91032d115cdff2a4a03834bb5f07a75ae606541bfd5ad8f51868fe824da7b8d6f06288a82d", + "padded_size": 1024, + "key_source": "c1 của bt-link.json", + "counter": 13, + "nonce": "000000000d00000000000000", + "ciphertext_tag": "f8fcc21f579b9620e55c5f05526115ddcb1db03cab55a93da8e8ef770b63f74d2def8fdfe11406b23e41bd808cc852f70991c0b883bac61221011ebd974f3bf4cf07113cd5b1c76d85395dcf96f0780948c089a1455ba96dbb783f80c896a3fcf94887483aa4b426380485bf2ba1fa7c608e998043c64a70047c4e0b38821f10be89af5f6c1d6876963139c78d99a5037d2647b0cee0637d850abb16ce90593b0dccb376e1ff2ed7e0a047cfe5db29cd9e96d15ccd883f71ad1300702d4211be0154020c297b775a537d1e7eb0c3810fca23da9fba1ded617696ef843f8c922524360313086a76badcf33688083c9b2829e4d1c7f2ddddf5e2eccf0c5d6e2104b0c75e12892d33bef24e97c4be12de8e6c2e83bd8acfa64b304e7adcfbbf720b6b7adf4662567323e087a398cad3eef292ce5daee1038ca39fb68924cabe67c42273935b8aad96f38b35c2b1a6e7334920c192a65e01b10e4548318787675df9060e7b61281658ad520b54d4d360e5bbcd6835ac58eadb8d30de4a581724fbf561edddc8f388611d8950a3a31c18a408e152a6354f9a5be29ad0a9b129f2d6f243e03a075b47287b2637dc7ec853a2f4a914245e4e6f56f09680329b92898fa5c3a60a1ab18a745d9752bde353f856b8fde86d5ffd1214f06f418dfb9a1c3ec321be2c9a192fd910700ff1c5b28bac062fd593a166a2c952f0d3ff53bf03a54ecb8cc6e15670a289921b8c0291ddc5236273e0080477a5733bdaee12b0d47d31b27ca9db333671ee4aaac54b77c1accbf65d6a3f529ee882753194454fc004978ac6c6efe6776f054dec698318914578de4f77c31aba41100a6ceb22149d0eb9869ebeefcdbe3d733fe6238849a35ee2b34e05769535b52c57b0048a3b400e69ad263a48aa39877cc12ce624f32f5c0abd5dc84a49915414f9ec5508f9268d8008212624ef20c72dd23d05e26800fcedb9bca3955d8ba95667814b5481f40d94b3e02ad3307119e6008b4b6b15e75ee91ac545f6bb766471715042a67f12b0f476fd9c1f103d7fb4ff1a39045e2900d7c36d4bb2995230d2649cebab95fc423c08fda8943eb6e6335699bc3ba88ac8f265d6a134910a7dcdd4764c1840bff9b140a3048bfe42b6b1cbc0706ed23bddce02365495c690b29b49ecd9945965e14a47dd030e82fd47a7713355ab79e26dcad2bb7a6d3b4b43e4b55a228557ea2e267fe2e80d1cb206cb7555b0fcfac753f7669cf624901f2e153a1e073f9a825c78e259bedc17b802a2b405e79ab5e0e0e45f1554287dc3614509c35f98f429c9ce296867661ab5968524b1ee6d88d2bb2077f328d503ff5f380f3c26cdecd0c6d9ed883d6294642decec7ee87bca462db47b0bc7380886fdf90258c7c3434dae16e2bce19ebb74b1d2a661f82db7ea440a861071df470f54ad2aa59692a8cc58dfe4f496edab0ec2a4e854b6c43f931fd4", + "rlen": 1040, + "record": "0410f8fcc21f579b9620e55c5f05526115ddcb1db03cab55a93da8e8ef770b63f74d2def8fdfe11406b23e41bd808cc852f70991c0b883bac61221011ebd974f3bf4cf07113cd5b1c76d85395dcf96f0780948c089a1455ba96dbb783f80c896a3fcf94887483aa4b426380485bf2ba1fa7c608e998043c64a70047c4e0b38821f10be89af5f6c1d6876963139c78d99a5037d2647b0cee0637d850abb16ce90593b0dccb376e1ff2ed7e0a047cfe5db29cd9e96d15ccd883f71ad1300702d4211be0154020c297b775a537d1e7eb0c3810fca23da9fba1ded617696ef843f8c922524360313086a76badcf33688083c9b2829e4d1c7f2ddddf5e2eccf0c5d6e2104b0c75e12892d33bef24e97c4be12de8e6c2e83bd8acfa64b304e7adcfbbf720b6b7adf4662567323e087a398cad3eef292ce5daee1038ca39fb68924cabe67c42273935b8aad96f38b35c2b1a6e7334920c192a65e01b10e4548318787675df9060e7b61281658ad520b54d4d360e5bbcd6835ac58eadb8d30de4a581724fbf561edddc8f388611d8950a3a31c18a408e152a6354f9a5be29ad0a9b129f2d6f243e03a075b47287b2637dc7ec853a2f4a914245e4e6f56f09680329b92898fa5c3a60a1ab18a745d9752bde353f856b8fde86d5ffd1214f06f418dfb9a1c3ec321be2c9a192fd910700ff1c5b28bac062fd593a166a2c952f0d3ff53bf03a54ecb8cc6e15670a289921b8c0291ddc5236273e0080477a5733bdaee12b0d47d31b27ca9db333671ee4aaac54b77c1accbf65d6a3f529ee882753194454fc004978ac6c6efe6776f054dec698318914578de4f77c31aba41100a6ceb22149d0eb9869ebeefcdbe3d733fe6238849a35ee2b34e05769535b52c57b0048a3b400e69ad263a48aa39877cc12ce624f32f5c0abd5dc84a49915414f9ec5508f9268d8008212624ef20c72dd23d05e26800fcedb9bca3955d8ba95667814b5481f40d94b3e02ad3307119e6008b4b6b15e75ee91ac545f6bb766471715042a67f12b0f476fd9c1f103d7fb4ff1a39045e2900d7c36d4bb2995230d2649cebab95fc423c08fda8943eb6e6335699bc3ba88ac8f265d6a134910a7dcdd4764c1840bff9b140a3048bfe42b6b1cbc0706ed23bddce02365495c690b29b49ecd9945965e14a47dd030e82fd47a7713355ab79e26dcad2bb7a6d3b4b43e4b55a228557ea2e267fe2e80d1cb206cb7555b0fcfac753f7669cf624901f2e153a1e073f9a825c78e259bedc17b802a2b405e79ab5e0e0e45f1554287dc3614509c35f98f429c9ce296867661ab5968524b1ee6d88d2bb2077f328d503ff5f380f3c26cdecd0c6d9ed883d6294642decec7ee87bca462db47b0bc7380886fdf90258c7c3434dae16e2bce19ebb74b1d2a661f82db7ea440a861071df470f54ad2aa59692a8cc58dfe4f496edab0ec2a4e854b6c43f931fd4" + }, + { + "name": "challenge (ví dụ đóng gói)", + "kind": "preauth_frame", + "preauth_kind": "0x01", + "body": "01d4ae169c229c47b6721c338524c7b8d543ba75c4765944c4abfe0752503f1623", + "record": "002101d4ae169c229c47b6721c338524c7b8d543ba75c4765944c4abfe0752503f1623" + }, + { + "name": "reject (ví dụ đóng gói, thân rỗng)", + "kind": "preauth_frame", + "preauth_kind": "0x7F", + "body": "7f", + "record": "00017f" + } + ], + "invalid_vectors": [ + { + "name": "rlen ngoài {272,1040,4112}", + "reason": "rlen_out_of_bounds", + "rlen": 273, + "record": "0111fc87296ac3ccb514c06d82912135d426ca2e07a1fc4a707b2ef2c943e8811b13fc899736fae24a23ab3f299fdbc525733e409428190c76d618c501e9f61987c39e28f16b35b0bbbeba8b7fecdc4d1bf3e084f0e495391dc92364c4200cebd0ab034b7507fad7702e8fc81ed737dc221fc3bea2c4c8e1c622991bd07d470e54bfc8587c9ea94070f77245ed6c939b35084ba43b79a03ef2968d53b9ba98ea62786031b96cde16f1dc45e5913f6fd6f3a3004e03373efac7f6ce0ca982987a2ef41ab66f7bf929f3bba92894a3f46669f0007ad3a17328aa1a4e5465e5743954f4028f599dd05b2780d1dce30e8d1a6f87382b784159397271d9c9f66bc7c834f7f0a8bcede83a311a0778643d712f9da900", + "expect": "rlen không khớp 272/1040/4112 → đóng kênh ngay, không cấp phát đọc tiếp thân bản ghi" + }, + { + "name": "bản ghi pre-auth vượt BT_PREAUTH_RECORD_MAX", + "reason": "preauth_record_over_bound", + "rlen": 65, + "record": "00410156f0efdacb55fd54712cb72f749aa05a62c48ba25f68dcba444d70664b69b1501d6b728e3717bb6f8da0c599234a4bfe16f3f336264c07d1056c40e2425cd050", + "bt_preauth_record_max": 64, + "expect": "rlen = 65 > BT_PREAUTH_RECORD_MAX (64) → đóng kênh ngay, không cấp phát" + }, + { + "name": "chan lạ (không 0x01/0x02)", + "reason": "unknown_chan", + "chan": "0x03", + "plaintext": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "record": "0110806908a31403380876b3b588eb1b26818968ffdb71e6de623d857f9b186abcf1fd3b00ce13c174ad9763530571c6adc9aa00da27c7412955d5465f214d120e7093b43bd59dd904058c99930a51b0a6cf9cfed57a2aa493d43a16dcbcab711acf3a5bb8f4c356b16fdcd1deed98fe4a52fe1c3e6a56e2641e61a69e40339e7af3d62a828266ce5b57209bbd50a484c0107c2707e1c0406a1d51abfd818c7aa3c87f9dc42f59b965dab4793b8e6dc708cb7d8b9986f460cd9aba016a66a3ae1f5fb0f859aad2e4ebf4130c4bb6474acf43919e38322e538876d6ee9b29971f86e96898e87f352fc9e674665ec4339843b300ff6f7c247d0f60d0ee9760f9a2118ed93268e941ed373347615a8126e0a252", + "expect": "chan = 0x03 không phải 0x01 (control) hay 0x02 (bulk) → đóng liên kết" + }, + { + "name": "more=1 nhưng long=0 (làn ngắn)", + "reason": "more_without_long", + "plaintext": "0101000aa78dd42688b494ca7e8c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "record": "01108e1f89a3cc0b6626c6d854c99ffb57faee1e58239e8fcd35a0056fa48c6ddb327f932f3e146594366bc05044adb4ab5fd8ecc93672cb75640a861743b78cddcffe58ebf23ee24e5ecec2baf6a21efadcb74552f9a6a9fd21a7da9024098f087c861a6a6fcbe98dbdd4c53fbd9f25cd3def99fae8e14d9b71543caa2dca6aaab984a2dc07d6c95f4991facdb1f7fb1aaddad3cb6e1ce753642f7783acb7ce35c91baf03f3c4f792728a95cab50f91cce59fbb87fe51cf04c98f569d6a717d3396080e17f2eb886fa051f4ca3aedaca72aefd40fa8c4774efd1605cc9ac8735d395a3fadf8eb0f9c41b6acd6a555039c5389a1e2058ed0926d68c191ffc007c6e6f4836b50d10073ec6c954f3dfa5fd9a2", + "expect": "làn ngắn (long=0) không bao giờ có more=1 (một làn ngắn luôn đúng một mảnh) → đóng liên kết" + }, + { + "name": "mảnh làn dài có dlen > BT_BULK_FRAGMENT_MAX (1020)", + "reason": "bulk_fragment_over_max", + "chan": "0x01", + "long": true, + "dlen": 1021, + "bt_bulk_fragment_max": 1020, + "expect": "dlen = 1021 > BT_BULK_FRAGMENT_MAX (1020) trên làn dài (long=1) hoặc làn bulk (chan=0x02) → đóng liên kết, không cần giải mã thân" + }, + { + "name": "tin làn dài vượt BT_MESSAGE_MAX trước mảnh cuối", + "reason": "message_over_max", + "chan": "0x01", + "long": true, + "bt_message_max": 262144, + "prior_bytes_received": 261644, + "last_fragment_dlen": 1020, + "total_after_fragment": 262664, + "expect": "261644 + 1020 = 262664 > BT_MESSAGE_MAX (262144) trước khi mảnh này là mảnh cuối (more=0) → đóng liên kết" + } + ] +} diff --git a/test-vectors/bt-link.json b/test-vectors/bt-link.json new file mode 100644 index 0000000..f72630f --- /dev/null +++ b/test-vectors/bt-link.json @@ -0,0 +1,150 @@ +{ + "description": "K_bt_disc = HKDF-SHA256(ikm = PRK, salt rỗng, info \"handlive/v1/bt-discovery\", L 32); K_bt_link (tương tự, info \"handlive/v1/bt-link\") — sống suốt đời cặp, khác K_bt_pdisc/K_bt_plink của bt-pairing.json (sống theo một QR). kind \"hint\": hint quảng bá bình thường (không phải lúc ghép đôi) = 4 byte đầu của HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE), epoch = floor(now_ms / BT_HINT_PERIOD) — khác pr_bt (đổi theo mỗi QR) vì hint này đổi theo thời gian (0.4.5 \"Advertisement\"). kind \"handshake\": bắt tay bảo mật liên kết đầy đủ (0.4.5 \"Link-security handshake\" bước 1-4), psk = K_bt_link: challenge (kind 0x01, n_s 32 byte) rồi message 1 (\"psk, e\", kind 0x02 noise1) và message 2 (\"e, ee\", kind 0x03 noise2) của Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), prologue = \"HLBT1\" ‖ n_s, rồi Split() ra c1 (C mã hoá/S giải mã)/c2 (ngược lại) — hai CipherState của bước 4-5. KHÔNG phải vector công bố của tài liệu Noise (xem ghi chú ở bt-pairing.json); noise_nnpsk0.py đã được Trang rà soát ở #3328 cho message 1, cần rà soát thêm phần message 2/Split mới. invalid_vectors: wrong_key, tag_mismatch_noise1, tag_mismatch_noise2 (0.4.5 bước 4: 'C verifies its payload tag — failure → C closes the channel, CONN-05 E3'), replayed_channel (0.4.5 bước 3: 'The prologue binds n_s, so a recorded noise1 never verifies on a new channel').", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Keys\", \"Advertisement\", \"Link-security handshake\"; PRK của pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": [ + { + "name": "cặp 1", + "kind": "key", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "prk_source": "pair-prk.json", + "prk": "d9e9827d49f7db11a88ac107ed6fb22b761c723c35449fc798694867fe55c209", + "k_bt_disc_info": "handlive/v1/bt-discovery", + "k_bt_disc": "c02c87d9df21fd5cf735a27ccb04de7f3ce79a76d9e958c300a0416bb0edeb35", + "k_bt_link_info": "handlive/v1/bt-link", + "k_bt_link": "16948c82ea6b9f3f4df8fdc711be84fb2ed360bf82cb8a60cf2808bc63548988" + }, + { + "name": "cặp 2", + "kind": "key", + "pair_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f2a3b4c5d", + "prk_source": "pair-prk.json", + "prk": "8d3109350469af80c3db25ff2c1867ab56112623020d829301236ed7251411cc", + "k_bt_disc_info": "handlive/v1/bt-discovery", + "k_bt_disc": "25add16aef33f9c7ec8a251e0bdae7bb91e80cb6c2ae66a8c763f83c628cb384", + "k_bt_link_info": "handlive/v1/bt-link", + "k_bt_link": "fcdddc4e4638ed616aba885ef26218e63e14f091f1e0c9ec840366877fea3d7d" + }, + { + "name": "cặp 1 / hint quảng bá", + "kind": "hint", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "k_bt_disc_source": "cặp 1", + "hint_label": "HLBTDISC1", + "bt_hint_period_ms": 900000, + "epochs": [ + { + "epoch": 0, + "hint": "72a1a227" + }, + { + "epoch": 1, + "hint": "b67a752b" + }, + { + "epoch": 2, + "hint": "4f40cd25" + }, + { + "epoch": 479762, + "hint": "1aeee44e" + } + ] + }, + { + "name": "cặp 1 / bắt tay liên kết thường", + "kind": "handshake", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "psk_source": "k_bt_link của cặp 1", + "psk": "16948c82ea6b9f3f4df8fdc711be84fb2ed360bf82cb8a60cf2808bc63548988", + "protocol_name": "Noise_NNpsk0_25519_ChaChaPoly_SHA256", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "prologue_label": "HLBT1", + "prologue": "484c4254312cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "challenge_kind": "0x01", + "challenge_record": "0021012cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "h0": "f0b890826fb8e6584af6948d227169c381c9ed6a4c1c90df4b9ff454fa947f7b", + "h_after_prologue": "e0220c0816e61954f8bf94de389c0786cdc9754de50446d37e2f0c9d3253da49", + "mix_key_and_hash_psk": { + "temp_key": "a726faeaaeb3c0d2e3d7cd0bc345318eae898b30c418654a64a9626623425f32", + "ck": "c6993109572a2d2e1142dc1bc4ce5e7d93c00ba623cca36b2a5f62308c1254c2", + "temp_h": "8b5a5ab1c9e9cded429858ed27b7c2213c80376f86cbdc98328acd0eac88b181", + "temp_k": "3493639e64b3e9889eab61e664b7b2db73e4c029e67411df3ef2b6f12769d663", + "h": "9b0e5a8a31cd403e0b2ef56bdee37911278e8000650ba8f6ec2dfb4efc122a92" + }, + "h_after_psk": "9b0e5a8a31cd403e0b2ef56bdee37911278e8000650ba8f6ec2dfb4efc122a92", + "ck_after_psk": "c6993109572a2d2e1142dc1bc4ce5e7d93c00ba623cca36b2a5f62308c1254c2", + "mix_key_e_c": { + "temp_key": "992aa59b6b45ff14691fbab352d1f1b6b6d558d971a1b9a3d6d8aeb6b9996d41", + "ck": "a496535f65bdea1ca2aedf11d16f1f8dc7128e23de83876fa11e7528a5d35192", + "temp_k": "79429352c7eaa1fb9f7534bf8e3a93c03a447e9c396ec2b2b57790f5129a6997" + }, + "ck_after_e_c": "a496535f65bdea1ca2aedf11d16f1f8dc7128e23de83876fa11e7528a5d35192", + "k_message1": "79429352c7eaa1fb9f7534bf8e3a93c03a447e9c396ec2b2b57790f5129a6997", + "e_priv_c": "6957d0f71dd22be6ea81d25694fd1abd2eed41d4fe5daa62af2bde2b3c8e957a", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "h_after_e_c": "bd7216420d0896066bb98d44504e3c4945af95b92188f1531744db2ada017941", + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f8", + "h_after_message1": "b9235dc4090d80a0d97d3bcf513e4c8d9d9beffbfb26071c2c77bf451b000371", + "message1": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960ff759c40f6fcf7a7ece21ec8aa86264f8", + "noise1_kind": "0x02", + "noise1_record": "0031023ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960ff759c40f6fcf7a7ece21ec8aa86264f8", + "e_priv_s": "e9948f4fd3e704551937595a00d3fe9b3fc018838941dc752e9cb6b9d63aba1b", + "e_pub_s": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a", + "h_after_e_s": "1fcef945c1f79c4b788742d7076d890aeeae6b9005a82e270f3131d4b7db168a", + "mix_key_e_s": { + "temp_key": "6b365205ebf84636b230df2dc9f8d6dc1cb871c6a083b44b641b0fe456b11047", + "ck": "4068d72ab3eb25403fc6058a5fe90e6ff9614ab18bcb29d421eedd92892f5a20", + "temp_k": "05a65608bb2cbce4a8b056e093747e6d7cdfbcb83904cf2e1f162e3df582c61a" + }, + "dh_ee": "3f592867b0e2821dc88dc14923ddf403ad7447ab4868207a60b812de220d5b14", + "mix_key_ee": { + "temp_key": "65bff845c1d0e8bb7cf80673bcfed09e0059f0b3e69d2ff944e0d3e4c3b47c08", + "ck": "0eb39273fe05c0f078de05abb581f220838d876a1d466d6d2fd7766d1ef9db89", + "temp_k": "8a2f696cf4d0e5833d501cca4f23dca6b46dcac0096f8b5973b2c1855da8db9a" + }, + "ck_after_ee": "0eb39273fe05c0f078de05abb581f220838d876a1d466d6d2fd7766d1ef9db89", + "k_message2": "8a2f696cf4d0e5833d501cca4f23dca6b46dcac0096f8b5973b2c1855da8db9a", + "ciphertext_tag_2": "ecc9c3f55d5df67b922f8f22177708ce", + "h_final": "df619d84c8e466cf942e4b4a561ccacd1a6cb0c18a5fbe71a1cc05eee6822bbb", + "message2": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3aecc9c3f55d5df67b922f8f22177708ce", + "noise2_kind": "0x03", + "noise2_record": "00310327c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3aecc9c3f55d5df67b922f8f22177708ce", + "c1": "a6d3dd0fccbbe0013127a75daa27ab07fd52c21cc7f50a3383fe60d3e5e10dbc", + "c2": "3288442eb16d11d2882265b638470fd1211854d19f7ff5f266606ed12ee8195e", + "c1_note": "c1 mã hoá bởi C (khởi tạo), giải mã bởi S", + "c2_note": "c2 mã hoá bởi S, giải mã bởi C" + } + ], + "invalid_vectors": [ + { + "name": "cặp 1 / S thử psk của cặp 2", + "reason": "wrong_key", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f8", + "wrong_psk_source": "k_bt_link của cặp 2", + "wrong_psk": "fcdddc4e4638ed616aba885ef26218e63e14f091f1e0c9ec840366877fea3d7d" + }, + { + "name": "cặp 1 / tag noise1 sai", + "reason": "tag_mismatch_noise1", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f9" + }, + { + "name": "cặp 1 / tag noise2 sai", + "reason": "tag_mismatch_noise2", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "e_pub_s": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a", + "ciphertext_tag_2": "ecc9c3f55d5df67b922f8f22177708cf" + }, + { + "name": "cặp 1 / phát lại noise1 trên kênh khác (n_s khác)", + "reason": "replayed_channel", + "n_s": "80982dfa4fb88f90dc856740bca0043664726aa1d2231f940819449553d808f1", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f8" + } + ] +} diff --git a/test-vectors/bt-pairing.json b/test-vectors/bt-pairing.json new file mode 100644 index 0000000..7d3fe87 --- /dev/null +++ b/test-vectors/bt-pairing.json @@ -0,0 +1,100 @@ +{ + "description": "K_bt_pdisc = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-disc\", L 32); K_bt_plink = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-link\", L 32); pr_bt = 4 byte đầu của HMAC-SHA256(K_bt_pdisc, \"HLBTPAIR1\"), đổi theo mỗi QR, không suy ra được pk/ps (0.4.5 \"Keys\", \"Pairing mode\"). kind \"first_frame\": message 1 (\"psk, e\") của Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), psk = K_bt_plink, prologue = \"HLBT1\" ‖ n_s — xác thực \"khung đầu\" của một liên kết ghép đôi trước khi slot pre-auth được cấp (0.4.5 \"Pairing mode\", \"Link-security handshake\"). HKDF ở đây là HKDF nội bộ của Noise (3 ngõ ra qua HMAC), khác HKDF-SHA256 RFC 5869 dùng ở các file khác trong kho này. `record` = rlen (uint16 BE) ‖ kind (0x02) ‖ message1 — đúng khuôn \"Link records\" của 0.4.5. KHÔNG phải vector công bố của tài liệu Noise (tài liệu đó không phát hành vector cho psk/prologue riêng của HandLive): đây là cách HandLive dùng đúng thuật toán của tài liệu đó cho bộ psk/prologue của mình, cần Trang rà soát riêng. Chưa có vector mode PIN (psk thay bằng K_bt_pinlink): cpace.json đã có ISK, chỉ là chưa nối vào, để lại cho một task sau.", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Keys\", \"Pairing mode\", \"Link-security handshake\"; pairing_secret của pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": [ + { + "name": "cặp 1", + "kind": "key", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "pairing_secret_source": "pair-prk.json", + "pairing_secret": "ded3ceb0d81908fe595a576826471c0cdcbedb8807c46e2e78ca4f1ab1499581", + "k_bt_pdisc_info": "handlive/v1/bt-pair-disc", + "k_bt_pdisc": "a5c9a25c3908f41632126d88910c7c1591e194e2c3b528c7f9f6021ac5648d00", + "k_bt_plink_info": "handlive/v1/bt-pair-link", + "k_bt_plink": "8954577d9f55cc77bae94e5ae3b2ca77c1c34a5153c1582e677c8ed1f8a36426", + "pr_bt_label": "HLBTPAIR1", + "pr_bt_mac": "24be2584931e152c8835dc782aa324353d2f941f3295a73c2e95419749b5af95", + "pr_bt": "24be2584" + }, + { + "name": "cặp 2", + "kind": "key", + "pair_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f2a3b4c5d", + "pairing_secret_source": "pair-prk.json", + "pairing_secret": "d6d0ab47c7cd93b2fd17308fc91509a0f1f334c50f2703c22228a27fa5d230e9", + "k_bt_pdisc_info": "handlive/v1/bt-pair-disc", + "k_bt_pdisc": "74801aec60b12c0408517afd9895fa64edba413dd73588c274f7f9fda1ec485c", + "k_bt_plink_info": "handlive/v1/bt-pair-link", + "k_bt_plink": "a705d680b0be1037fc8ac0dd02c2803f7e55795564df81f55b5e9fc5c6364cec", + "pr_bt_label": "HLBTPAIR1", + "pr_bt_mac": "38dbf4483c7fa2fb62c87bab65e7ff36dfa6bc9bd0223d653dfda073808d4814", + "pr_bt": "38dbf448" + }, + { + "name": "cặp 1 / liên kết ghép đôi QR", + "kind": "first_frame", + "mode": "qr", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "psk_source": "k_bt_plink của cặp 1", + "psk": "8954577d9f55cc77bae94e5ae3b2ca77c1c34a5153c1582e677c8ed1f8a36426", + "protocol_name": "Noise_NNpsk0_25519_ChaChaPoly_SHA256", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "prologue_label": "HLBT1", + "prologue": "484c425431641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "h0": "f0b890826fb8e6584af6948d227169c381c9ed6a4c1c90df4b9ff454fa947f7b", + "h_after_prologue": "88d9f0b6de0bce978824376d99b038449919fbb63a9b4cb84de8c826eac9ea57", + "mix_key_and_hash": { + "temp_key": "d1fe5993d0f49c69dd7535cee9ef6e4ef644e183f5f49f0c55666ac9e059eda4", + "ck": "3ba0261b99a6ab88fb8fee3f6204b310b6edbaf59aad56d8e676fe1835bca22e", + "temp_h": "b62999756f94b21d0cd7c9518479d3681fe7f8e80219c3e93212449bc7157502", + "temp_k": "de6117d2b12eead9c6b282d772b909920916d8fb1ef43bd1caa24ab037606311", + "h": "9ef7ca8694e03435f4cdc5de84c258bbb69179dc685f817c0139739589f08199" + }, + "h_after_psk": "9ef7ca8694e03435f4cdc5de84c258bbb69179dc685f817c0139739589f08199", + "ck_after_psk": "0fedd0a3f07e1abd54695065fdc83462fc2b2ef89a2532509c6090648aaca51e", + "k": "9491cbf574b16d2b036724c0f8566f335935fa899230540ec6beb83db64ae27b", + "e_priv": "9373bf265905f54866bd4566421bcf20bb7c6bf255e0afbdf6df27a6fd873d7b", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "h_after_e": "92ce9e8eb3761a90c9602e7b0b38dfff8855c89bd3bd6a7cacdbccf7ed669428", + "nonce": "000000000000000000000000", + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35", + "h_final": "ab1cf064715202bb93ab8cf45f43de39ab492e17ee8ccbe79954fe9e17075e25", + "message1": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a55001e0ca15311784bfa449d0d7b498db35", + "record_kind": "0x02", + "rlen": 49, + "record": "003102676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a55001e0ca15311784bfa449d0d7b498db35" + } + ], + "invalid_vectors": [ + { + "name": "cặp 1 / S thử psk của cặp 2", + "reason": "wrong_key", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35", + "wrong_psk_source": "k_bt_plink của cặp 2", + "wrong_psk": "a705d680b0be1037fc8ac0dd02c2803f7e55795564df81f55b5e9fc5c6364cec" + }, + { + "name": "cặp 1 / tag sai", + "reason": "tag_mismatch", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db34" + }, + { + "name": "cặp 1 / e_pub bị sửa trên đường truyền", + "reason": "message_tampered", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "e_pub": "666f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35" + }, + { + "name": "cặp 1 / phát lại message 1 trên kênh khác (n_s khác)", + "reason": "aad_mismatch", + "n_s": "5300745f6a02d27bb53c2c9852d204fcfbae14f9bfd39fc3165dfebacc2c0c6f", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35" + } + ] +} diff --git a/test-vectors/cpace.json b/test-vectors/cpace.json new file mode 100644 index 0000000..052458f --- /dev/null +++ b/test-vectors/cpace.json @@ -0,0 +1,128 @@ +{ + "description": "CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512). kind \"official_generator\"/\"official_message\": vector công bố của draft (Appendix B.1), chép nguyên văn qua cpace_source_values.py, dùng để kiểm cài đặt calculate_generator/Ya/Yb/K/ISK đúng thuật toán trước khi áp case của HandLive. kind \"handlive_pin\": ghép đôi bằng PIN qua Bluetooth không mạng (0.4.5 luồng C) — PRS = UTF-8 của 6 chữ số PIN (giữ số 0 đứng đầu), CI = \"HLBTPIN1\" cố định, sid = n_s (32 byte, mới cho mỗi kênh), ADa = ADb rỗng; C khởi tạo (cpace1 = Ya), S trả lời (cpace2 = Yb); K_bt_pinlink/K_bt_pin = HKDF-SHA256(ikm = ISK, salt = n_s, info tương ứng, L 32). invalid_vectors: low_order_ya/low_order_yb (bên kia nhận một điểm bậc thấp của Appendix B.1.10 → K = G.I → phải đóng kênh không trả lời, đúng \"an all-zero K → close without answer\" của 0.4.5), sid_mismatch (hai phía dùng n_s khác nhau → ISK khác nhau), wrong_pin (PIN gõ sai một phía → generator khác nhau → ISK khác nhau hoặc K toàn 0) — cả hai trường hợp sau không bị CPace tự abort, chỉ lộ ra khi noise1 (psk = K_bt_pinlink) không mở được ở bước ghép đôi tiếp theo (bt-pairing.json, chưa nối case PIN vào — còn một việc để làm sau).", + "source": "draft-irtf-cfrg-cpace-21 Appendix B.1 (CPACE-X25519-SHA512); docs/detailed-design/00-common-specs.md 0.4.5 \"PIN (PAIR-01 flow C)\", \"Keys from ISK\"", + "vectors": [ + { + "name": "draft B.1.1 calculate_generator", + "kind": "official_generator", + "draft": "draft-irtf-cfrg-cpace-21", + "section": "Appendix B.1.1", + "H": "SHA-512", + "PRS": "50617373776f7264", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "generator_string": "0843506163653235350850617373776f72646d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180b415f696e69746961746f720b425f726573706f6e646572107e4b4791d6a8ef019b936c79fb7f2c57", + "hash_generator_string": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed1d3", + "decoded_field_element": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed153", + "g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f" + }, + { + "name": "draft B.1.2-B.1.7 message A/B, K, ISK", + "kind": "official_message", + "draft": "draft-irtf-cfrg-cpace-21", + "section": "Appendix B.1.2-B.1.7", + "PRS": "50617373776f7264", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f", + "ya": "21b4f4bd9e64ed355c3eb676a28ebedaf6d8f17bdc365995b319097153044080", + "ADa": "414461", + "Ya": "1d13c89278cdadd826f6d8d7f887701430f8380ddc17611cdd6dc989ce0c9f32", + "yb": "848b0779ff415f0af4ea14df9dd1d3c29ac41d836c7808896c4eba19c51ac40a", + "ADb": "414462", + "Yb": "248cccf6d5cdc3646f0ad593f9e6cef4e69d4945f8372e623512ecea32185623", + "K": "5b067effbdc0b2a0e1d907b21ebb25cfedb96a852179a847c37e43ee71322c6b", + "ISK_initiator_responder": "6e19b875f7a561d6b3ca3dbb9ef42ac55de3e717881018204b8922b4d5e53bb2aa82c300bea7b65d2b671da71922ddf6472301b79bc270adfa8bf413285f2263", + "ISK_parallel_source": "eef745e2f6e7ae2b1a1e53da340e777167a07fe150436648c51fb199c11f3cbabfc683a2b48e1af5881940dc398d375c95e6b4ae9948a45b8770de0656382be4", + "note": "ISK_parallel_source là biến thể song hành (transcript_oc) công bố trong draft; HandLive chỉ dùng khởi tạo/trả lời (transcript_ir, ISK_initiator_responder) vì C luôn khởi tạo và S luôn trả lời (0.4.5 luồng C)." + }, + { + "name": "HandLive PIN 482917", + "kind": "handlive_pin", + "pin": "482917", + "ci": "HLBTPIN1", + "n_s": "d9ffef6cbff35962ba8a34204c9120e28c38ea8359e142a2078093e16174db9f", + "ya": "967a4f831f84d786d4b93add9c4df87970a7f755d988458e623d647e2b475756", + "yb": "7055ce569105f131eb4e136022b31813a2935be018132e34876a5d305a595e38", + "g": "dbd7b61972017c48ce9fdf0b27680bc473cd5c1b249c70da989f93921b194c4a", + "Ya": "4f55e241e6868d6248c26c3265ea833af2590ed8e000a069c1b15cf9f387b028", + "Yb": "65e7198a167a05e257689343d539b1135249c1ef184498e6f05345c749dc6876", + "K": "f5e92fa92192e3757d91871da7e9b3378db7f7863d480a218297bfd7f7dd8c4c", + "ISK": "a140d1555c39acfade8e5614f01f3f653e48bac557cb8696aa6e3cae697ddd4d1ac4bd465bb46d4b03e626eda18c22afc68027f2483639199033e5015d06559d", + "k_bt_pinlink_info": "handlive/v1/bt-pin-link", + "k_bt_pinlink": "c9c3410f6cef4b6f9f171728a89e9e5a957ae69565a479aacb33ae5d5333919d", + "k_bt_pin_info": "handlive/v1/bt-pin-pair", + "k_bt_pin": "697f1ab82b22fdb4fa9058e03ea4ef1ae0f83f04496c48586c06517faf8bc011" + }, + { + "name": "HandLive PIN với số 0 đứng đầu", + "kind": "handlive_pin", + "pin": "007042", + "ci": "HLBTPIN1", + "n_s": "649a041524d8e3283aecd1ae96bf77b4b8a891f0f64919dca1cb5b0299e793e7", + "ya": "59b192ebcd0c08df5eeb4c8d60e5dfc3a2a4a73eb8e947a1698f2f202a44728a", + "yb": "e75f7662275c3291e81e8b3a1a0de45762a4ddadec1bf8f6df0d99355572916a", + "g": "11d8f2ec1d5f75970b2e29e3450ff03adf35fbbc0052b4e0fb182fb2283fa762", + "Ya": "b73f7368964e21efa50a78985d9fe9a3b60811a33e286bb7b55cb5f418861732", + "Yb": "681dcca28dbc603a7d278fea8e0b5ddd8eafa19f364cae9c7a31d30a6b56c40e", + "K": "6e2372e1549d0903cd98a42cb60433fd749dcd6b336da102337a9a5f7c01bc20", + "ISK": "49e3b485cdb41c789d0517e73b1fe8f55c9e8c5b5438cfc91d744fb35066f9da9cee2af9e2a0d7172ca019f4e07718ec0e4f9e0d2e1c9b04be13f4bda62b40b2", + "k_bt_pinlink_info": "handlive/v1/bt-pin-link", + "k_bt_pinlink": "ff1ec28fb791604a5463b07b4a766e79a969957a5f261926c372e7759a30172f", + "k_bt_pin_info": "handlive/v1/bt-pin-pair", + "k_bt_pin": "68d6c9d67c60edb2e24829dbadcb3bb8a579850287d896cb4ed405d54f7840ef" + } + ], + "invalid_vectors": [ + { + "name": "Ya bậc thấp (C gửi điểm bậc thấp)", + "reason": "low_order_ya", + "pin": "482917", + "n_s": "39b2916f56d70c3a961b0c5c9a529554296e1444895a7b3f4d92bd7fe6b6c975", + "g": "ae90317d0ec527b03bfe2913bba8894a4867d9e9108c2211ceb9cebb67dd8c38", + "Ya_attacker": "0000000000000000000000000000000000000000000000000000000000000000", + "yb": "24a846ac94b2877f88b6fbc05d26bc34a6b39eca3f4111f86495bb545d7c072c", + "Yb": "70fc3700ef310a827f960e44a3789e658bf193af4de502ddaf903c17d45f1b50", + "expect": "S (trả lời) tính K = scalar_mult_vfy(yb, Ya) → None (G.I) → đóng kênh không trả lời" + }, + { + "name": "Yb bậc thấp (S trả lời điểm bậc thấp)", + "reason": "low_order_yb", + "pin": "482917", + "n_s": "f45b17823a5be592c77a25787d33d1e933468b16d6351e63ff694cbf21005ac8", + "g": "ac838292b96d3819195dd0e603e64db81e13bb201af332e55508510e11a9b962", + "ya": "87c62e1364379d2e7c38eca52153ff6fbd3ff23eac4d08995d3082df3f0b7221", + "Ya": "97443720baeff9822c713bc744c9b27b5d99addfc89b24dd31ef5ecf7e748c04", + "Yb_attacker": "0000000000000000000000000000000000000000000000000000000000000000", + "expect": "C (khởi tạo) tính K = scalar_mult_vfy(ya, Yb) → None (G.I) → đóng kênh không trả lời" + }, + { + "name": "sid khác nhau giữa C và S", + "reason": "sid_mismatch", + "pin": "482917", + "n_s_c": "e87431400d2326edce7bffa661f2410750593a7440da45318ebda492ee50e83b", + "n_s_s": "63013053536e4e93452b14122d1a20eb050b299b57614bc5a11036367955959f", + "ya": "a624ee31fddf2d27bed5f94805862a950cce7e8ff55b5c8a294c9739d8493d3b", + "yb": "7d9db7d0df243d74b0c2a11faa0b816c393a60995aedecefafec418f8be44ef1", + "Ya": "b2873620a0f94f86829caac85574aeb235cae323659be55ffe9de735353ea12c", + "Yb": "4172d1fa46dc332c8c67bb3d1ea7a4e36837bfc2ec4af634e09f1148059d1140", + "isk_c": "1abba784dcb2299e3a9268b7e1e63c1b7827e679b82a2c2556c9354f0486cb4bde38a7ff41dc3769eda23fd568d328719117e9170da98cec47458854fbe63694", + "isk_s": "b0c2c262912fd60f3034203566e7e4f8d12cc1fc2d5b5daada407181a8d319369a65437799f9b185b222d6d28b7c61728ce8bdaaf147b4a486f08ade01c87cf9", + "expect": "C dùng sid của mình, S dùng sid của mình (hai kênh khác nhau trộn vào) → ISK hai phía khác nhau (isk_c != isk_s), K_bt_pinlink suy từ đó cũng khác → noise1 của S không mở được" + }, + { + "name": "PIN sai ở một phía", + "reason": "wrong_pin", + "n_s": "a703f54bfb6373687d58b76bce23bcf8b703fd038e93a165ca19c06981e64338", + "pin_c": "482917", + "pin_s": "482918", + "ya": "3741bd1815d80f12d33a99f7a1af3472a902059b10737199d51e8ff9261eaea6", + "yb": "e31412bdc16c0c3387c341735477c6e25567e984287547e157b6ecf85f7415c4", + "Ya": "e01e299f6deb35b29bfc479c7c41efeef7c4285d6fee38f5b9f439c4c4ed9a75", + "Yb": "f7d79dec08cd392efea84b0042f3870a38a61d935c95c078609048491a729a08", + "isk_c": "945d17603e9ce161f8f9a99e56401fc60354c05e588442b9a6a49dbb26fc710d1d51f5cc82609827f14b15a4cb0210b791674826a7decef29d57e9aece3edc97", + "isk_s": "e7ec60648010bea911bb0a32a404af342628f932468f56ceca3aa5322da2ca970d43baff73e5df304a04e5b083ac35044042621178f6ffe4f7512102f27eeee2", + "expect": "PIN gõ sai ở một phía → generator khác nhau → ISK hai phía khác nhau (hoặc K toàn 0) → noise1 dùng K_bt_pinlink sai không mở được, một lần thử bị tính vào bộ đếm chung" + } + ] +} diff --git a/tools/bench/README.md b/tools/bench/README.md index 865e2b5..e687c6d 100644 --- a/tools/bench/README.md +++ b/tools/bench/README.md @@ -71,6 +71,14 @@ Values never contain spaces. Anything before the marker `HLBENCH/1 ` (logcat or | `net` | Any | OS network change: Android `ConnectivityManager.NetworkCallback`, Apple `NWPathMonitor` | `change`: `up` (a network is available), `down` (lost), `changed` (default network switched) | | `wake` | Mac | `NSWorkspace.didWakeNotification` | — | +### Bluetooth and Wi-Fi Direct events (Phase 7, 0.4.5/0.4.6) + +| `ev` | Who | When | Fields | +|------|-----|------|--------| +| `bt_seen` | Mac (M-APP, central) | A BLE scan result matches a candidate pair's rotating hint or `BT_PIN_HINT` (0.4.5 "Advertisement") — a reconnect trigger, like `wake`/`net up` | — | +| `bt_lost` | Either side | An L2CAP channel closes (idle timeout, `reject`, a key-confirmation failure, the OS tearing it down) | `reason` (`idle_timeout`, `reject`, `key_confirmation`, `os`) — informational only, never a `reconnect_time.py` trigger: the loss itself is already in `state` | +| `wifi_assoc` | Mac | The Mac associates with or leaves the phone's Wi-Fi Direct group (0.4.6) | `change`: `up` (joined), `down` (left) — a reconnect trigger like `net` | + ### SMS events (Phase 2) Privacy as above: only the provider's `message_key` (`sms:<_id>`), the random `local_id`, states and error codes — never the text, the number or a name. diff --git a/tools/bench/README.vi.md b/tools/bench/README.vi.md index 7800dd4..0dfca6b 100644 --- a/tools/bench/README.vi.md +++ b/tools/bench/README.vi.md @@ -71,6 +71,14 @@ Giá trị không chứa khoảng trắng. Mọi thứ trước dấu `HLBENCH/1 | `net` | Mọi thiết bị | Hệ điều hành báo mạng đổi: Android `ConnectivityManager.NetworkCallback`, Apple `NWPathMonitor` | `change`: `up` (có mạng), `down` (mất mạng), `changed` (đổi mạng mặc định) | | `wake` | Mac | `NSWorkspace.didWakeNotification` | — | +### Sự kiện Bluetooth và Wi-Fi Direct (Phase 7, 0.4.5/0.4.6) + +| `ev` | Ai | Khi nào | Trường | +|------|----|---------|--------| +| `bt_seen` | Mac (M-APP, vai trung tâm) | Kết quả quét BLE khớp hint xoay vòng của một cặp ứng viên hoặc `BT_PIN_HINT` (0.4.5 "Advertisement") — một trigger kết nối lại, như `wake`/`net up` | — | +| `bt_lost` | Bên nào cũng được | Một kênh L2CAP đóng (hết giờ idle, `reject`, xác thực khóa thất bại, hệ điều hành tự đóng) | `reason` (`idle_timeout`, `reject`, `key_confirmation`, `os`) — chỉ để biết, không bao giờ là trigger của `reconnect_time.py`: việc mất kết nối đã có trong `state` rồi | +| `wifi_assoc` | Mac | Mac vào hoặc ra khỏi group Wi-Fi Direct của điện thoại (0.4.6) | `change`: `up` (đã vào), `down` (đã ra) — một trigger kết nối lại như `net` | + ### Sự kiện SMS (Phase 2) Quyền riêng tư như trên: chỉ `message_key` của provider (`sms:<_id>`), `local_id` ngẫu nhiên, trạng thái và mã lỗi — không bao giờ có nội dung tin, số điện thoại hay tên. diff --git a/tools/bench/bench_log.py b/tools/bench/bench_log.py index b035fd4..89f6428 100644 --- a/tools/bench/bench_log.py +++ b/tools/bench/bench_log.py @@ -27,6 +27,11 @@ "state": {"to"}, "net": {"change"}, "wake": set(), + # Bluetooth link (P7, S7.1a): a scan match (reconnect trigger, like wake/net), a closed link (informational, + # not a trigger — the loss is already in `state`), and the Mac joining/leaving a Wi-Fi Direct group (trigger). + "bt_seen": set(), + "bt_lost": {"reason"}, + "wifi_assoc": {"change"}, # SMS (Phase 2): new SMS notification (SMS-02) and reply confirmed as Sent (SMS-04); see README.md. "sms_detected": {"msg", "box", "onchange"}, "sms_new_sent": {"msg", "peer", "via"}, diff --git a/tools/bench/reconnect_time.py b/tools/bench/reconnect_time.py index 63935b5..49c4ccc 100644 --- a/tools/bench/reconnect_time.py +++ b/tools/bench/reconnect_time.py @@ -3,10 +3,13 @@ python3 tools/bench/reconnect_time.py mac.log android.log [--offset A:B=MS] [--json] [--check] An episode runs on a client (Mac, iPhone, iPad) from a `state` event that leaves Connected to the next one that -enters Connected (00-common-specs 0.11). Its trigger is the last `net` event with change=up or changed, or the -last `wake` event, on any device inside the episode — times of other devices are put on the client's clock -(clock_sync.py). Reconnect time = Connected minus trigger; without a trigger (the phone's service restarted, -the pong timed out) only the time since the loss is shown. Target: under 3000 ms on the 95th percentile. +enters Connected (00-common-specs 0.11). Its trigger is the last `net` or `wifi_assoc` event with change=up or +changed, or the last `wake` or `bt_seen` event, on any device inside the episode — times of other devices are put +on the client's clock (clock_sync.py). `bt_seen` (a Bluetooth scan match, 0.4.5) and `wifi_assoc` (the Mac joining +a Wi-Fi Direct group, 0.4.6) are triggers like `wake`/`net up`; `bt_lost` is informational only (the loss itself +is already in `state`) and never a trigger. Reconnect time = Connected minus trigger; without a trigger (the +phone's service restarted, the pong timed out) only the time since the loss is shown. Target: under 3000 ms on +the 95th percentile. """ from __future__ import annotations @@ -41,7 +44,9 @@ class Episode: def episodes(log: Log, clocks: ClockModel) -> list[Episode]: out = [] clients = sorted({e.dev for e in log.events if e.ev == "state" and e.role in CLIENT_ROLES}) - triggers = [e for e in log.events if (e.ev == "net" and e.get("change") in ("up", "changed")) or e.ev == "wake"] + triggers = [e for e in log.events + if (e.ev in ("net", "wifi_assoc") and e.get("change") in ("up", "changed")) + or e.ev in ("wake", "bt_seen")] for client in clients: states = [e for e in log.events if e.dev == client and e.ev == "state"] lost: Event | None = None @@ -68,7 +73,7 @@ def _episode(client: str, up_since: Event, lost: Event, connected: Event, trigge if best is None: trigger, reconnect = "loss", None else: - what = "wake" if best.ev == "wake" else f"net {best.get('change')}" + what = best.ev if best.ev in ("wake", "bt_seen") else f"{best.ev} {best.get('change')}" trigger = f"{what} on {best.dev}" reconnect = (local_interval_ms(best, connected) if best.dev == client else connected.wall_ms - best_on_client) diff --git a/tools/bench/self_test.py b/tools/bench/self_test.py index 8854766..436cf60 100644 --- a/tools/bench/self_test.py +++ b/tools/bench/self_test.py @@ -75,7 +75,9 @@ def build_logs() -> tuple[dict, dict]: logs[IPAD].append(line(IPAD, 30_016.5, "ack_sent", clip=fwd, peer=PHONE, status="applied")) logs[PHONE].append(line(PHONE, 30_020.5, "ack_received", clip=fwd, peer=IPAD, status="applied")) truth["forward-ipad"] = 30_016 - 30_000 - # Reconnects on the Mac: its own Wi-Fi switch, then the phone's, then a pong timeout without trigger. + # Reconnects on the Mac: its own Wi-Fi switch, then the phone's, then a pong timeout without trigger, then a + # Bluetooth scan match (P7, 0.4.5), then the Mac joining a Wi-Fi Direct group (0.4.6); a bt_lost in between + # is informational only and must not be picked up as a trigger. for true_ms, ev, fields in [(0, "state", {"to": "Discovering"}), (400, "state", {"to": "Connected", "channel": "lan"}), (40_000, "net", {"change": "down"}), (40_100, "state", {"from": "Connected", "to": "Backoff"}), (41_900, "net", {"change": "up"}), (42_000, "state", {"to": "Discovering"}), @@ -83,10 +85,17 @@ def build_logs() -> tuple[dict, dict]: (60_000, "state", {"from": "Connected", "to": "Backoff"}), (64_700, "state", {"to": "Connected", "channel": "lan"}), (80_000, "state", {"from": "Connected", "to": "Backoff"}), - (80_900, "state", {"to": "Connected", "channel": "lan"})]: + (80_900, "state", {"to": "Connected", "channel": "lan"}), + (90_000, "state", {"from": "Connected", "to": "Backoff"}), + (91_500, "bt_seen", {}), + (92_200, "state", {"to": "Connected", "channel": "bluetooth"}), + (100_000, "state", {"from": "Connected", "to": "Backoff"}), + (101_000, "wifi_assoc", {"change": "up"}), + (101_800, "state", {"to": "Connected", "channel": "lan"}), + (101_900, "bt_lost", {"reason": "idle_timeout"})]: logs[MAC].append(line(MAC, true_ms, ev, **fields)) logs[PHONE].append(line(PHONE, 62_500, "net", change="up")) - truth["reconnect"] = [(43_150 - 41_900), (64_700 - 62_500), None] + truth["reconnect"] = [(43_150 - 41_900), (64_700 - 62_500), None, (92_200 - 91_500), (101_800 - 101_000)] # A clip the Mac refused (conflict, QC8): read and sent, never applied. lost = "0192f3e0-0000-7000-8000-00000000000f" logs[PHONE].append(line(PHONE, 50_000, "clip_read", clip=lost, kind="text", bytes=10, source="manual")) @@ -172,12 +181,18 @@ def main() -> int: eps = reconnect_time.episodes(log, clocks) got = [e.reconnect_ms for e in eps] - t.check("three reconnect episodes", len(eps) == 3, str(eps)) + t.check("five reconnect episodes", len(eps) == 5, str(eps)) t.check("Mac Wi-Fi switch", close(got[0], truth["reconnect"][0]) and eps[0].trigger == f"net up on {MAC}", str(eps[0])) t.check("phone Wi-Fi switch on the Mac's clock", close(got[1], truth["reconnect"][1]), str(eps[1])) t.check("episode without trigger keeps only the time since the loss", got[2] is None and close(eps[2].since_loss_ms, 900.0), str(eps[2])) + t.check("Bluetooth scan match (bt_seen) as trigger", + close(got[3], truth["reconnect"][3]) and eps[3].trigger == f"bt_seen on {MAC}", str(eps[3])) + t.check("Wi-Fi Direct group join (wifi_assoc) as trigger", + close(got[4], truth["reconnect"][4]) and eps[4].trigger == f"wifi_assoc up on {MAC}", str(eps[4])) + t.check("bt_lost event present and not counted as an extra episode", + len(log.of("bt_lost")) == 1 and log.of("bt_lost")[0].get("reason") == "idle_timeout", str(log.of("bt_lost"))) code, text = run([str(p) for p in paths] + ["--check"], reconnect_time.main) t.check("reconnect target met", code == 0 and "PASS" in text, text) diff --git a/tools/setup-cpace-refs.sh b/tools/setup-cpace-refs.sh new file mode 100755 index 0000000..57a9ada --- /dev/null +++ b/tools/setup-cpace-refs.sh @@ -0,0 +1,151 @@ +#!/bin/zsh +# Chay NGOAI sandbox (can mang ra ngoai): zsh handlive-shared/tools/setup-cpace-refs.sh +# +# Buoc 1: tai van ban chinh thuc (.txt) cua draft-irtf-cfrg-cpace, ban moi nhat, tu IETF datatracker, +# luu vao tools/vectors/.refs/draft-irtf-cfrg-cpace.txt (khong commit — xem .gitignore). +# Buoc 2: voi moi file requirements*.txt duoi tools/, dung mot .venv rieng ngay canh file do +# (tools/vectors/.venv, tools/schemas/.venv, tools/strings/.venv, tools/bench/.venv, tools/e2e/.venv, +# tools/e2e/relay_stack/.venv...) va pip install -r vao do. +# +# In ro tung buoc thanh cong/that bai; thoat 0 chi khi CA HAI buoc deu xong. + +SHARED_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DRAFT_NAME="draft-irtf-cfrg-cpace" +REFS_DIR="$SHARED_ROOT/tools/vectors/.refs" +DRAFT_OUT="$REFS_DIR/${DRAFT_NAME}.txt" + +step1_ok=1 +step2_ok=1 + +echo "== Buoc 1: tai ${DRAFT_NAME} (.txt, ban moi nhat) tu IETF datatracker ==" +mkdir -p "$REFS_DIR" + +REV="" +API_JSON="$(curl -fsS "https://datatracker.ietf.org/api/v1/doc/document/${DRAFT_NAME}/" 2>/dev/null)" +if [ -n "$API_JSON" ]; then + REV="$(printf '%s' "$API_JSON" | python3 -c 'import json, sys +try: + print(json.load(sys.stdin).get("rev", "")) +except Exception: + pass' 2>/dev/null)" +fi + +if [ -z "$REV" ]; then + echo " API datatracker khong tra duoc 'rev', thu doc trang tai lieu (HTML) de do phien ban moi nhat..." + HTML="$(curl -fsS "https://datatracker.ietf.org/doc/${DRAFT_NAME}/" 2>/dev/null)" + REV="$(printf '%s' "$HTML" | grep -o "${DRAFT_NAME}-[0-9][0-9]" | head -1 | sed 's/.*-//')" +fi + +if [ -z "$REV" ]; then + echo " LOI buoc 1: khong xac dinh duoc phien ban moi nhat cua ${DRAFT_NAME} (API va trang HTML cua datatracker deu khong tra duoc 'rev'). Kiem lai mang ra hoac ten tai lieu." + step1_ok=0 +else + TXT_URL="https://www.ietf.org/archive/id/${DRAFT_NAME}-${REV}.txt" + echo " Phien ban moi nhat: -${REV}; tai tu ${TXT_URL}" + HTTP_CODE="$(curl -sS -w '%{http_code}' -o "${DRAFT_OUT}.tmp" "$TXT_URL")" + CURL_EXIT=$? + if [ "$CURL_EXIT" -ne 0 ] || [ "$HTTP_CODE" != "200" ]; then + echo " LOI buoc 1: tai ${TXT_URL} that bai (curl exit=${CURL_EXIT}, http=${HTTP_CODE})." + rm -f "${DRAFT_OUT}.tmp" + step1_ok=0 + else + mv "${DRAFT_OUT}.tmp" "$DRAFT_OUT" + echo " OK: da luu ${DRAFT_OUT} ($(wc -c < "$DRAFT_OUT" | tr -d ' ') byte)." + fi +fi + +echo +echo "== Buoc 2: dung .venv rieng cho tung thu muc tools/* co requirements*.txt, pip install -r ==" +REQ_FILES="$(find "$SHARED_ROOT/tools" -iname 'requirements*.txt' | sort)" +if [ -z "$REQ_FILES" ]; then + echo " LOI buoc 2: khong thay file requirements*.txt nao duoi $SHARED_ROOT/tools" + step2_ok=0 +else + while IFS= read -r REQ; do + [ -z "$REQ" ] && continue + DIR="$(dirname "$REQ")" + VENV="$DIR/.venv" + REL="${DIR#"$SHARED_ROOT"/}" + echo " -- $REL ($(basename "$REQ")) --" + if [ ! -x "$VENV/bin/python3" ]; then + echo " tao venv moi: $VENV/" + if ! python3 -m venv "$VENV"; then + echo " LOI: 'python3 -m venv' that bai cho $VENV" + step2_ok=0 + continue + fi + else + echo " venv da co san, dung lai: $VENV/" + fi + echo " pip install -r $REQ" + if ! "$VENV/bin/pip" install --quiet --upgrade-strategy only-if-needed -r "$REQ"; then + echo " LOI: pip install that bai cho $REQ" + step2_ok=0 + continue + fi + echo " OK" + done <<< "$REQ_FILES" +fi + +step3_ok=1 + +echo +echo "== Buoc 3: chay thử verify_vectors.py, check_schemas.py, check_strings.py bang .venv vua dung ==" +VERIFY_SCRIPT="$SHARED_ROOT/tools/vectors/verify_vectors.py" +CHECK_SCHEMAS="$SHARED_ROOT/tools/schemas/check_schemas.py" +CHECK_STRINGS="$SHARED_ROOT/tools/strings/check_strings.py" + +if [ -f "$VERIFY_SCRIPT" ] && [ -x "$SHARED_ROOT/tools/vectors/.venv/bin/python3" ]; then + echo " Chay verify_vectors.py..." + if "$SHARED_ROOT/tools/vectors/.venv/bin/python3" "$VERIFY_SCRIPT"; then + echo " OK: verify_vectors.py xong." + else + echo " LOI: verify_vectors.py loi." + step3_ok=0 + fi +else + echo " SKIP: verify_vectors.py khong tim thay hoac venv khong san sang." + step3_ok=0 +fi + +if [ -f "$CHECK_SCHEMAS" ] && [ -x "$SHARED_ROOT/tools/schemas/.venv/bin/python3" ]; then + echo " Chay check_schemas.py..." + if "$SHARED_ROOT/tools/schemas/.venv/bin/python3" "$CHECK_SCHEMAS"; then + echo " OK: check_schemas.py xong." + else + echo " LOI: check_schemas.py loi." + step3_ok=0 + fi +else + echo " SKIP: check_schemas.py khong tim thay hoac venv khong san sang." + step3_ok=0 +fi + +if [ -f "$CHECK_STRINGS" ] && [ -x "$SHARED_ROOT/tools/strings/.venv/bin/python3" ]; then + echo " Chay check_strings.py..." + if "$SHARED_ROOT/tools/strings/.venv/bin/python3" "$CHECK_STRINGS"; then + echo " OK: check_strings.py xong." + else + echo " LOI: check_strings.py loi." + step3_ok=0 + fi +else + echo " SKIP: check_strings.py khong tim thay hoac venv khong san sang." + step3_ok=0 +fi + +echo +if [ "$step1_ok" -eq 1 ] && [ "$step2_ok" -eq 1 ] && [ "$step3_ok" -eq 1 ]; then + echo "XONG: ca 3 buoc thanh cong." + exit 0 +fi +if [ "$step1_ok" -eq 0 ]; then + echo "LOI: buoc 1 (tai draft) that bai." +fi +if [ "$step2_ok" -eq 0 ]; then + echo "LOI: buoc 2 (venv + pip install) that bai." +fi +if [ "$step3_ok" -eq 0 ]; then + echo "LOI: buoc 3 (thuc hien verify/check) that bai hoac khong tim thay script." +fi +exit 1 diff --git a/tools/vectors/bt_frame_codec.py b/tools/vectors/bt_frame_codec.py new file mode 100644 index 0000000..579b3be --- /dev/null +++ b/tools/vectors/bt_frame_codec.py @@ -0,0 +1,46 @@ +"""Mã hoá/giải mã khung Bluetooth (00-common-specs.md 0.4.5 "Link records", "Transport records and channels") — +dùng cho cả phía sinh và phía kiểm (chỉ là phép toán đóng gói byte, không phải thuật toán mật mã, nên không cần +hai cài đặt độc lập — giống cách lv_cat/LEB128 của CPace được chia sẻ).""" + +PADDED_SIZES = (256, 1024, 4096) +TAG_LEN = 16 +RECORD_SIZES = tuple(p + TAG_LEN for p in PADDED_SIZES) # 272, 1040, 4112 +CHAN_CONTROL, CHAN_BULK = 0x01, 0x02 +FLAG_MORE, FLAG_LONG = 0x01, 0x02 +PREAUTH_RECORD_MAX = 64 + + +def padded_size(dlen: int) -> int: + """Cỡ nhỏ nhất trong {256, 1024, 4096} chứa đủ khung chan(1)+flags(1)+dlen(2)+data(dlen) (0.4.5).""" + frame_len = 4 + dlen + for size in PADDED_SIZES: + if frame_len <= size: + return size + raise ValueError(f"dlen={dlen} vượt cả 4096 (BT_FRAGMENT_MAX=4092 phải chặn trước khi gọi hàm này)") + + +def build_frame(chan: int, more: bool, long_lane: bool, data: bytes) -> bytes: + """chan(1) ‖ flags(1) ‖ dlen(2 BE) ‖ data ‖ padding 0, tới cỡ padded_size(len(data)).""" + flags = (FLAG_MORE if more else 0) | (FLAG_LONG if long_lane else 0) + header = bytes([chan, flags]) + len(data).to_bytes(2, "big") + plaintext = header + data + size = padded_size(len(data)) + return plaintext + bytes(size - len(plaintext)) + + +def parse_frame(plaintext: bytes) -> dict: + chan, flags = plaintext[0], plaintext[1] + dlen = int.from_bytes(plaintext[2:4], "big") + data = plaintext[4:4 + dlen] + padding = plaintext[4 + dlen:] + return {"chan": chan, "more": bool(flags & FLAG_MORE), "long": bool(flags & FLAG_LONG), "dlen": dlen, + "data": data, "padding": padding} + + +def build_record(rlen_body: bytes) -> bytes: + """rlen (uint16 BE) ‖ rlen bytes — khung chung cho mọi bản ghi (0.4.5 "Link records").""" + return len(rlen_body).to_bytes(2, "big") + rlen_body + + +def build_preauth_record(kind: int, body: bytes) -> bytes: + return build_record(bytes([kind]) + body) diff --git a/tools/vectors/bt_link_derivations.py b/tools/vectors/bt_link_derivations.py new file mode 100644 index 0000000..1121f9f --- /dev/null +++ b/tools/vectors/bt_link_derivations.py @@ -0,0 +1,24 @@ +"""K_bt_disc/K_bt_link dẫn từ `PRK` của pair-prk.json (00-common-specs.md 0.4.5 "Keys") — dùng để SINH vector. +Khác `K_bt_pdisc`/`K_bt_plink` (bt_pairing_derivations.py, dẫn từ `pairing_secret`, chỉ sống trong một QR): các +khoá này sống suốt đời cặp, dùng cho liên kết Bluetooth bình thường sau khi đã ghép đôi.""" +from handlive_protocol_derivations import hkdf, hmac256 + +DISC_INFO = b"handlive/v1/bt-discovery" +LINK_INFO = b"handlive/v1/bt-link" +HINT_LABEL = b"HLBTDISC1" + + +def k_bt_disc(prk: bytes) -> bytes: + """K_bt_disc = HKDF-SHA256(ikm = PRK, salt rỗng, info "handlive/v1/bt-discovery", L = 32).""" + return hkdf(prk, DISC_INFO, 32) + + +def k_bt_link(prk: bytes) -> bytes: + """K_bt_link = HKDF-SHA256(ikm = PRK, salt rỗng, info "handlive/v1/bt-link", L = 32).""" + return hkdf(prk, LINK_INFO, 32) + + +def disc_hint(k_bt_disc_key: bytes, epoch: int) -> bytes: + """hint quảng bá = 4 byte đầu của HMAC-SHA256(K_bt_disc, "HLBTDISC1" ‖ epoch int64 BE) (0.4.5 "Advertisement").""" + msg = HINT_LABEL + epoch.to_bytes(8, "big", signed=True) + return hmac256(k_bt_disc_key, msg)[:4] diff --git a/tools/vectors/bt_pairing_derivations.py b/tools/vectors/bt_pairing_derivations.py new file mode 100644 index 0000000..686a076 --- /dev/null +++ b/tools/vectors/bt_pairing_derivations.py @@ -0,0 +1,22 @@ +"""K_bt_pdisc/K_bt_plink và pr_bt (00-common-specs.md 0.4.5 "Keys", "Pairing mode"), dẫn từ `pairing_secret` của +pair-prk.json — dùng để SINH vector.""" +from handlive_protocol_derivations import hkdf, hmac256 + +PDISC_INFO = b"handlive/v1/bt-pair-disc" +PLINK_INFO = b"handlive/v1/bt-pair-link" +PR_BT_LABEL = b"HLBTPAIR1" + + +def k_bt_pdisc(pairing_secret: bytes) -> bytes: + """K_bt_pdisc = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info "handlive/v1/bt-pair-disc", L = 32).""" + return hkdf(pairing_secret, PDISC_INFO, 32) + + +def k_bt_plink(pairing_secret: bytes) -> bytes: + """K_bt_plink = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info "handlive/v1/bt-pair-link", L = 32).""" + return hkdf(pairing_secret, PLINK_INFO, 32) + + +def pr_bt(pdisc_key: bytes) -> bytes: + """pr_bt = 4 byte đầu của HMAC-SHA256(K_bt_pdisc, "HLBTPAIR1").""" + return hmac256(pdisc_key, PR_BT_LABEL)[:4] diff --git a/tools/vectors/build_bt_advertisement_vectors.py b/tools/vectors/build_bt_advertisement_vectors.py new file mode 100644 index 0000000..468ec4c --- /dev/null +++ b/tools/vectors/build_bt_advertisement_vectors.py @@ -0,0 +1,85 @@ +"""Dựng bt-advertisement.json: byte service data mà app tự kiểm soát trong scan response (0.4.5 "Advertisement") — +`ver` ‖ `psm` (uint16 LE) ‖ `hints` (4 × n, n ≤ BT_ADV_MAX_HINTS); hệ điều hành tự thêm phần còn lại của cấu trúc +AD (cờ, UUID 128-bit) nên không thuộc phạm vi vector này (xem #3337: service_data_bytes log 11, cộng 18 của OS +mới ra 29). Cộng `BT_PIN_HINT` khi đang mở cửa sổ ghép đôi PIN, và mẫu âm cho luật khớp hint ở client. +""" +import bt_link_derivations as BT + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +BT_ADV_MAX_HINTS = 2 +BT_PIN_HINT = bytes(4) +AD_UUID_STRUCT_BYTES = 18 # AD length + AD type + UUID 128-bit, hệ điều hành tự thêm +VER = 0x01 + + +def _service_data(psm: int, hints: list) -> bytes: + return bytes([VER]) + psm.to_bytes(2, "little") + b"".join(hints) + + +def _scan_response_vector(name: str, psm: int, hints: list, pair_names: list) -> dict: + data = _service_data(psm, hints) + total = AD_UUID_STRUCT_BYTES + len(data) + return {"name": name, "kind": "scan_response", "ver": f"0x{VER:02x}", "psm": psm, "psm_hex": psm.to_bytes(2, "little").hex(), + "hints": [h.hex() for h in hints], "hint_pairs": pair_names, "service_data_bytes": data.hex(), + "service_data_len": len(data), "ad_uuid_struct_bytes": AD_UUID_STRUCT_BYTES, + "scan_response_total": total, "note": f"service_data_bytes = phần app tự kiểm soát; OS thêm " + f"{AD_UUID_STRUCT_BYTES} byte UUID/cờ nữa mới ra tổng {total} byte"} + + +def _negatives(disc: bytes) -> list[dict]: + out = [] + data = _service_data(23456, [BT.disc_hint(disc, 1000)]) + bad_ver = bytearray(data) + bad_ver[0] = 0x02 + out.append({"name": "ver khác 0x01", "reason": "wrong_version", "service_data_bytes": bytes(bad_ver).hex(), + "expect": "ver = 0x02 ≠ 0x01 → client bỏ qua toàn bộ advertisement, không xét hint"}) + + client_epoch = 1000 + accepted = list(range(client_epoch - 4, client_epoch + 5)) + outside_epoch = client_epoch + 5 + outside_hint = BT.disc_hint(disc, outside_epoch) + out.append({"name": "hint ngoài cửa sổ e-4..e+4", "reason": "hint_outside_window", + "client_epoch": client_epoch, "accepted_epochs": accepted, "advertised_epoch": outside_epoch, + "advertised_hint": outside_hint.hex(), "service_data_bytes": _service_data(23456, [outside_hint]).hex(), + "expect": f"advertised_epoch = {outside_epoch} cách client_epoch {outside_epoch - client_epoch} kỷ " + "nguyên, ngoài [e-4, e+4] → client không thấy khớp hint nào"}) + return out + + +DESCRIPTION = ( + "Byte service data app tự kiểm soát trong scan response (0.4.5 \"Advertisement\"): `ver` (1, luôn 0x01) ‖ " + "`psm` (2, uint16 LE, PSM L2CAP hiện tại) ‖ `hints` (4 × n, n = 0…BT_ADV_MAX_HINTS = 2, mỗi hint = 4 byte đầu " + "của HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE)) — K_bt_disc/hint lấy từ bt-link.json. OS thêm 18 " + "byte cấu trúc AD (cờ + UUID 128-bit) để ra tổng scan response (21/25/29 byte cho n=0/1/2) — không thuộc " + "phạm vi vector này, chỉ ghi `scan_response_total` để đối chiếu bảng 0.4.5. `kind` = \"pin_window\": `hints` " + "= chỉ `BT_PIN_HINT` (0x00000000), đánh dấu cửa sổ ghép đôi PIN đang mở, giống nhau cho mọi điện thoại (AC9). " + "invalid_vectors: wrong_version (ver ≠ 0x01 → bỏ qua cả advertisement), hint_outside_window (hint tính ở kỷ " + "nguyên ngoài [e-4, e+4] của client → không khớp)." +) + + +def build(ctx: dict, files: dict) -> dict: + link_doc = files["bt-link.json"] + key_vectors = [v for v in link_doc["vectors"] if v["kind"] == "key"] + disc = H(next(v for v in key_vectors if v["name"] == "cặp 1")["k_bt_disc"]) + disc2 = H(next(v for v in key_vectors if v["name"] == "cặp 2")["k_bt_disc"]) + + psm = 23456 + hint1, hint2 = BT.disc_hint(disc, 2000), BT.disc_hint(disc2, 2000) + vectors = [ + _scan_response_vector("0 hint (không cặp nào có session nào, không có cửa sổ QR)", psm, [], []), + _scan_response_vector("1 hint (một cặp gần đây nhất)", psm, [hint1], ["cặp 1"]), + _scan_response_vector("2 hint (BT_ADV_MAX_HINTS, hai cặp gần đây nhất)", psm, [hint1, hint2], + ["cặp 1", "cặp 2"]), + {"name": "cửa sổ PIN đang mở (BT_PIN_HINT)", "kind": "pin_window", "ver": f"0x{VER:02x}", "psm": psm, + "psm_hex": psm.to_bytes(2, "little").hex(), "hints": [BT_PIN_HINT.hex()], + "service_data_bytes": _service_data(psm, [BT_PIN_HINT]).hex(), + "service_data_len": len(_service_data(psm, [BT_PIN_HINT])), + "note": "BT_PIN_HINT giống nhau cho mọi điện thoại, không gắn với cặp hay thiết bị nào (AC9)"}, + ] + negatives = _negatives(disc) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Advertisement\"; K_bt_disc của bt-link.json", + "vectors": vectors, "invalid_vectors": negatives} + return {"bt-advertisement.json": doc} diff --git a/tools/vectors/build_bt_frame_vectors.py b/tools/vectors/build_bt_frame_vectors.py new file mode 100644 index 0000000..bd9fdec --- /dev/null +++ b/tools/vectors/build_bt_frame_vectors.py @@ -0,0 +1,136 @@ +"""Dựng bt-frame.json: khung bản ghi transport (chan/flags/dlen/data/padding, 0.4.5 "Transport records and +channels") mã hoá bằng `c1` của bt-link.json (Noise transport message, nonce = bộ đếm 64-bit LE), cùng mẫu âm cho +từng luật cận mà A7.1a/M7.1b phải chặn: `rlen` ngoài {272, 1 040, 4 112}, `chan` lạ, `more` không có `long`, một +mảnh làn dài vượt `BT_BULK_FRAGMENT_MAX`, một tin làn dài vượt `BT_MESSAGE_MAX` trước mảnh cuối, một bản ghi +pre-auth vượt `BT_PREAUTH_RECORD_MAX`. +""" +import bt_frame_codec as F +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +BT_FRAGMENT_MAX = 4092 +BT_BULK_FRAGMENT_MAX = 1020 +BT_MESSAGE_MAX = 256 * 1024 +BT_PREAUTH_RECORD_MAX = 64 + + +def _nonce(counter: int) -> bytes: + return bytes(4) + counter.to_bytes(8, "little") + + +def _encrypt(key: bytes, counter: int, plaintext: bytes) -> bytes: + return ChaCha20Poly1305(key).encrypt(_nonce(counter), plaintext, b"") + + +def _frame_vector(name: str, chan: int, more: bool, long_lane: bool, data: bytes, key: bytes, counter: int) -> dict: + plaintext = F.build_frame(chan, more, long_lane, data) + ciphertext_tag = _encrypt(key, counter, plaintext) + record = F.build_record(ciphertext_tag) + return {"name": name, "kind": "transport_frame", "chan": f"0x{chan:02x}", "more": more, "long": long_lane, + "dlen": len(data), "data": data.hex(), "plaintext": plaintext.hex(), "padded_size": len(plaintext), + "key_source": "c1 của bt-link.json", "counter": counter, "nonce": _nonce(counter).hex(), + "ciphertext_tag": ciphertext_tag.hex(), "rlen": len(ciphertext_tag), "record": record.hex()} + + +def _preauth_vectors() -> list[dict]: + challenge_body = bytes([0x01]) + test_bytes("bt frame preauth example n_s", 32) + reject_body = bytes([0x7F]) + return [ + {"name": "challenge (ví dụ đóng gói)", "kind": "preauth_frame", "preauth_kind": "0x01", + "body": challenge_body.hex(), "record": F.build_preauth_record(0x01, challenge_body[1:]).hex()}, + {"name": "reject (ví dụ đóng gói, thân rỗng)", "kind": "preauth_frame", "preauth_kind": "0x7F", + "body": reject_body.hex(), "record": F.build_preauth_record(0x7F, b"").hex()}, + ] + + +def _negatives(key: bytes) -> list[dict]: + out = [] + + good_plain = F.build_frame(F.CHAN_CONTROL, False, False, test_bytes("bt frame rlen_over_bound data", 100)) + good_ct = _encrypt(key, 900, good_plain) + assert len(good_ct) in F.RECORD_SIZES + bad_rlen_body = good_ct + b"\x00" # 273, không nằm trong {272,1040,4112} + out.append({"name": "rlen ngoài {272,1040,4112}", "reason": "rlen_out_of_bounds", + "rlen": len(bad_rlen_body), "record": F.build_record(bad_rlen_body).hex(), + "expect": "rlen không khớp 272/1040/4112 → đóng kênh ngay, không cấp phát đọc tiếp thân bản ghi"}) + + preauth_body = bytes([0x01]) + test_bytes("bt frame preauth_over_bound", BT_PREAUTH_RECORD_MAX) + assert len(preauth_body) == BT_PREAUTH_RECORD_MAX + 1 > BT_PREAUTH_RECORD_MAX + out.append({"name": "bản ghi pre-auth vượt BT_PREAUTH_RECORD_MAX", + "reason": "preauth_record_over_bound", "rlen": len(preauth_body), + "record": F.build_record(preauth_body).hex(), "bt_preauth_record_max": BT_PREAUTH_RECORD_MAX, + "expect": f"rlen = {len(preauth_body)} > BT_PREAUTH_RECORD_MAX ({BT_PREAUTH_RECORD_MAX}) → đóng " + "kênh ngay, không cấp phát"}) + + bad_chan_plain = bytes([0x03, 0x00]) + (0).to_bytes(2, "big") + bytes(252) + bad_chan_ct = _encrypt(key, 901, bad_chan_plain) + out.append({"name": "chan lạ (không 0x01/0x02)", "reason": "unknown_chan", "chan": "0x03", + "plaintext": bad_chan_plain.hex(), "record": F.build_record(bad_chan_ct).hex(), + "expect": "chan = 0x03 không phải 0x01 (control) hay 0x02 (bulk) → đóng liên kết"}) + + more_no_long_plain = bytes([F.CHAN_CONTROL, F.FLAG_MORE]) + (10).to_bytes(2, "big") + \ + test_bytes("bt frame more_without_long", 10) + bytes(256 - 14) + more_no_long_ct = _encrypt(key, 902, more_no_long_plain) + out.append({"name": "more=1 nhưng long=0 (làn ngắn)", "reason": "more_without_long", + "plaintext": more_no_long_plain.hex(), "record": F.build_record(more_no_long_ct).hex(), + "expect": "làn ngắn (long=0) không bao giờ có more=1 (một làn ngắn luôn đúng một mảnh) → đóng " + "liên kết"}) + + over_fragment_data = test_bytes("bt frame bulk_fragment_over_max data", BT_BULK_FRAGMENT_MAX + 1) + out.append({"name": f"mảnh làn dài có dlen > BT_BULK_FRAGMENT_MAX ({BT_BULK_FRAGMENT_MAX})", + "reason": "bulk_fragment_over_max", "chan": "0x01", "long": True, + "dlen": len(over_fragment_data), "bt_bulk_fragment_max": BT_BULK_FRAGMENT_MAX, + "expect": f"dlen = {len(over_fragment_data)} > BT_BULK_FRAGMENT_MAX ({BT_BULK_FRAGMENT_MAX}) trên " + "làn dài (long=1) hoặc làn bulk (chan=0x02) → đóng liên kết, không cần giải mã thân"}) + + prior = BT_MESSAGE_MAX - 500 + last_fragment_dlen = BT_BULK_FRAGMENT_MAX + out.append({"name": "tin làn dài vượt BT_MESSAGE_MAX trước mảnh cuối", "reason": "message_over_max", + "chan": "0x01", "long": True, "bt_message_max": BT_MESSAGE_MAX, + "prior_bytes_received": prior, "last_fragment_dlen": last_fragment_dlen, + "total_after_fragment": prior + last_fragment_dlen, + "expect": f"{prior} + {last_fragment_dlen} = {prior + last_fragment_dlen} > BT_MESSAGE_MAX " + f"({BT_MESSAGE_MAX}) trước khi mảnh này là mảnh cuối (more=0) → đóng liên kết"}) + return out + + +DESCRIPTION = ( + "Khung bản ghi pre-auth (`rlen` uint16 BE ‖ `rlen` byte, kind đầu tiên của thân — 0.4.5 \"Link records\") và " + "khung control/bulk sau bước 4 (chan(1) ‖ flags(1, bit0 more/bit1 long) ‖ dlen(2 BE) ‖ data ‖ 0-padding tới " + "256/1 024/4 096 byte, mã hoá Noise transport = ChaCha20-Poly1305(key, nonce = 4 zero ‖ bộ đếm 64-bit LE, " + "AAD rỗng) ‖ tag 16 byte — rlen hợp lệ duy nhất 272/1 040/4 112). `key_source` = `c1` của bt-link.json (hướng " + "C→S của cùng bắt tay). invalid_vectors khớp đúng 6 luật cận của 0.4.5/S7.1a: rlen_out_of_bounds (rlen không " + "thuộc {272,1040,4112} → đóng kênh trước khi cấp phát đọc thêm), preauth_record_over_bound (bản ghi pre-auth " + "vượt BT_PREAUTH_RECORD_MAX = 64 byte), unknown_chan (chan khác 0x01/0x02), more_without_long (more=1 trên " + "làn ngắn, long=0), bulk_fragment_over_max (dlen một mảnh làn dài/bulk vượt BT_BULK_FRAGMENT_MAX = 1 020), " + "message_over_max (tổng byte một tin làn dài vượt BT_MESSAGE_MAX = 256 KiB trước mảnh cuối)." +) + + +def build(ctx: dict, files: dict) -> dict: + link_doc = files["bt-link.json"] + handshake = next(v for v in link_doc["vectors"] if v["kind"] == "handshake") + c1 = H(handshake["c1"]) + + short_data = test_bytes("bt frame short control example", 40) + short_vec = _frame_vector("control / làn ngắn (long=0, 1 mảnh)", F.CHAN_CONTROL, False, False, short_data, + c1, 10) + + long_data_1 = test_bytes("bt frame long control example 1", BT_BULK_FRAGMENT_MAX) + long_vec_1 = _frame_vector("control / làn dài, mảnh 1/2 (more=1, long=1)", F.CHAN_CONTROL, True, True, + long_data_1, c1, 11) + long_data_2 = test_bytes("bt frame long control example 2", 400) + long_vec_2 = _frame_vector("control / làn dài, mảnh 2/2 cuối (more=0, long=1)", F.CHAN_CONTROL, False, True, + long_data_2, c1, 12) + + bulk_data = test_bytes("bt frame bulk example", BT_BULK_FRAGMENT_MAX) + bulk_vec = _frame_vector("bulk / một mảnh ảnh (more=1)", F.CHAN_BULK, True, False, bulk_data, c1, 13) + + vectors = [short_vec, long_vec_1, long_vec_2, bulk_vec] + _preauth_vectors() + negatives = _negatives(c1) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Link records\", \"Transport records and channels\"; c1 của bt-link.json", + "vectors": vectors, "invalid_vectors": negatives} + return {"bt-frame.json": doc} diff --git a/tools/vectors/build_bt_link_vectors.py b/tools/vectors/build_bt_link_vectors.py new file mode 100644 index 0000000..5313e4d --- /dev/null +++ b/tools/vectors/build_bt_link_vectors.py @@ -0,0 +1,140 @@ +"""Dựng bt-link.json: K_bt_disc/K_bt_link (00-common-specs.md 0.4.5 "Keys"), hint quảng bá xoay vòng, và bắt tay +bảo mật liên kết đầy đủ (`challenge`/`noise1`/`noise2`, psk = `K_bt_link`) cho một liên kết Bluetooth bình thường +(sau khi đã ghép đôi) — khác bt-pairing.json (psk = `K_bt_plink`, chỉ cho liên kết ghép đôi QR). +""" +import bt_frame_codec as F +import bt_link_derivations as BT +import noise_nnpsk0 as N +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +PROLOGUE_LABEL = b"HLBT1" +BT_HINT_PERIOD_MS = 900_000 + + +def _key_vector(pv: dict) -> dict: + prk = H(pv["prk"]) + disc, link = BT.k_bt_disc(prk), BT.k_bt_link(prk) + return {"name": pv["name"], "kind": "key", "pair_id": pv["pair_id"], "prk_source": "pair-prk.json", + "prk": prk.hex(), "k_bt_disc_info": BT.DISC_INFO.decode(), "k_bt_disc": disc.hex(), + "k_bt_link_info": BT.LINK_INFO.decode(), "k_bt_link": link.hex()} + + +def _hint_vector(pv: dict, disc: bytes) -> dict: + """Hint ở 3 kỷ nguyên liên tiếp: client chấp nhận hint trong khoảng e-4..e+4 (0.4.5 "Advertisement").""" + epochs = [0, 1, 2, 479762] + hints = [{"epoch": e, "hint": BT.disc_hint(disc, e).hex()} for e in epochs] + return {"name": f"{pv['name']} / hint quảng bá", "kind": "hint", "pair_id": pv["pair_id"], + "k_bt_disc_source": pv["name"], "hint_label": BT.HINT_LABEL.decode(), + "bt_hint_period_ms": BT_HINT_PERIOD_MS, "epochs": hints} + + +def _handshake_vector(pv: dict, link: bytes) -> dict: + n_s = test_bytes(f"bt link n_s {pv['name']}", 32) + challenge_record = F.build_preauth_record(0x01, n_s) + prologue = PROLOGUE_LABEL + n_s + + e_priv_c = test_bytes(f"bt link e_priv_c {pv['name']}", 32) + m1 = N.first_message(link, prologue, e_priv_c) + opened = N.try_open_first_message(link, prologue, m1["e_pub"], m1["ciphertext_tag"]) + assert opened == b"", f"{pv['name']}: noise1 phải mở được với đúng psk" + noise1_record = F.build_preauth_record(0x02, m1["message1"]) + + e_priv_s = test_bytes(f"bt link e_priv_s {pv['name']}", 32) + m2 = N.second_message(m1["ck1"], m1["h4"], e_priv_s, m1["e_pub"]) + pt_c, ck2_c, h6_c = N.try_open_second_message(m1["ck1"], m1["h4"], e_priv_c, m2["e_pub"], m2["ciphertext_tag"]) + assert pt_c == b"" and ck2_c == m2["ck2"] and h6_c == m2["h6"], f"{pv['name']}: noise2 phải khớp cả hai phía" + noise2_record = F.build_preauth_record(0x03, m2["message2"]) + + split_s = N.split(m2["ck2"]) + split_c = N.split(ck2_c) + assert split_s == split_c, f"{pv['name']}: Split() phải khớp cả hai phía" + + return { + "name": f"{pv['name']} / bắt tay liên kết thường", "kind": "handshake", "pair_id": pv["pair_id"], + "psk_source": f"k_bt_link của {pv['name']}", "psk": link.hex(), "protocol_name": m1["protocol_name"].decode(), + "n_s": n_s.hex(), "prologue_label": PROLOGUE_LABEL.decode(), "prologue": prologue.hex(), + "challenge_kind": "0x01", "challenge_record": challenge_record.hex(), + "h0": m1["h0"].hex(), "h_after_prologue": m1["h1"].hex(), + "mix_key_and_hash_psk": {k: v.hex() for k, v in m1["mix_key_and_hash"].items()}, + "h_after_psk": m1["h2"].hex(), "ck_after_psk": m1["ck_after_psk"].hex(), + "mix_key_e_c": {k: v.hex() for k, v in m1["mix_key_e"].items()}, + "ck_after_e_c": m1["ck1"].hex(), "k_message1": m1["k"].hex(), + "e_priv_c": m1["e_priv"].hex(), "e_pub_c": m1["e_pub"].hex(), "h_after_e_c": m1["h3"].hex(), + "ciphertext_tag_1": m1["ciphertext_tag"].hex(), "h_after_message1": m1["h4"].hex(), + "message1": m1["message1"].hex(), "noise1_kind": "0x02", "noise1_record": noise1_record.hex(), + "e_priv_s": m2["e_priv"].hex(), "e_pub_s": m2["e_pub"].hex(), "h_after_e_s": m2["h5"].hex(), + "mix_key_e_s": {k: v.hex() for k, v in m2["mix_key_e"].items()}, "dh_ee": m2["dh"].hex(), + "mix_key_ee": {k: v.hex() for k, v in m2["mix_key_ee"].items()}, + "ck_after_ee": m2["ck2"].hex(), "k_message2": m2["k"].hex(), "ciphertext_tag_2": m2["ciphertext_tag"].hex(), + "h_final": m2["h6"].hex(), "message2": m2["message2"].hex(), "noise2_kind": "0x03", + "noise2_record": noise2_record.hex(), "c1": split_s["c1"].hex(), "c2": split_s["c2"].hex(), + "c1_note": "c1 mã hoá bởi C (khởi tạo), giải mã bởi S", "c2_note": "c2 mã hoá bởi S, giải mã bởi C", + } + + +def _negatives(pv1: dict, pv2: dict, link1: bytes, link2: bytes, hs: dict) -> list[dict]: + n_s, prologue = H(hs["n_s"]), H(hs["prologue"]) + e_pub_c, ct1 = H(hs["e_pub_c"]), H(hs["ciphertext_tag_1"]) + e_pub_s, ct2 = H(hs["e_pub_s"]), H(hs["ciphertext_tag_2"]) + ck1, h4 = H(hs["ck_after_e_c"]), H(hs["h_after_message1"]) + out = [] + + assert N.try_open_first_message(link2, prologue, e_pub_c, ct1) is None + out.append({"name": f"{pv1['name']} / S thử psk của {pv2['name']}", "reason": "wrong_key", "n_s": hs["n_s"], + "e_pub_c": hs["e_pub_c"], "ciphertext_tag_1": hs["ciphertext_tag_1"], + "wrong_psk_source": f"k_bt_link của {pv2['name']}", "wrong_psk": link2.hex()}) + + bad_ct1 = bytearray(ct1) + bad_ct1[-1] ^= 0x01 + assert N.try_open_first_message(link1, prologue, e_pub_c, bytes(bad_ct1)) is None + out.append({"name": f"{pv1['name']} / tag noise1 sai", "reason": "tag_mismatch_noise1", "n_s": hs["n_s"], + "e_pub_c": hs["e_pub_c"], "ciphertext_tag_1": bytes(bad_ct1).hex()}) + + bad_ct2 = bytearray(ct2) + bad_ct2[-1] ^= 0x01 + pt_bad, _, _ = N.try_open_second_message(ck1, h4, H(hs["e_priv_c"]), e_pub_s, bytes(bad_ct2)) + assert pt_bad is None + out.append({"name": f"{pv1['name']} / tag noise2 sai", "reason": "tag_mismatch_noise2", "n_s": hs["n_s"], + "e_pub_s": hs["e_pub_s"], "ciphertext_tag_2": bytes(bad_ct2).hex()}) + + other_n_s = test_bytes(f"bt link n_s kênh khác {pv1['name']}", 32) + assert N.try_open_first_message(link1, PROLOGUE_LABEL + other_n_s, e_pub_c, ct1) is None + out.append({"name": f"{pv1['name']} / phát lại noise1 trên kênh khác (n_s khác)", "reason": "replayed_channel", + "n_s": other_n_s.hex(), "e_pub_c": hs["e_pub_c"], "ciphertext_tag_1": hs["ciphertext_tag_1"]}) + return out + + +DESCRIPTION = ( + "K_bt_disc = HKDF-SHA256(ikm = PRK, salt rỗng, info \"handlive/v1/bt-discovery\", L 32); K_bt_link (tương tự, " + "info \"handlive/v1/bt-link\") — sống suốt đời cặp, khác K_bt_pdisc/K_bt_plink của bt-pairing.json (sống theo " + "một QR). kind \"hint\": hint quảng bá bình thường (không phải lúc ghép đôi) = 4 byte đầu của " + "HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE), epoch = floor(now_ms / BT_HINT_PERIOD) — khác pr_bt " + "(đổi theo mỗi QR) vì hint này đổi theo thời gian (0.4.5 \"Advertisement\"). kind \"handshake\": bắt tay bảo " + "mật liên kết đầy đủ (0.4.5 \"Link-security handshake\" bước 1-4), psk = K_bt_link: challenge (kind 0x01, n_s " + "32 byte) rồi message 1 (\"psk, e\", kind 0x02 noise1) và message 2 (\"e, ee\", kind 0x03 noise2) của " + "Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), prologue = \"HLBT1\" ‖ n_s, rồi " + "Split() ra c1 (C mã hoá/S giải mã)/c2 (ngược lại) — hai CipherState của bước 4-5. KHÔNG phải vector công bố " + "của tài liệu Noise (xem ghi chú ở bt-pairing.json); noise_nnpsk0.py đã được Trang rà soát ở #3328 cho message " + "1, cần rà soát thêm phần message 2/Split mới. invalid_vectors: wrong_key, tag_mismatch_noise1, " + "tag_mismatch_noise2 (0.4.5 bước 4: 'C verifies its payload tag — failure → C closes the channel, CONN-05 E3'), " + "replayed_channel (0.4.5 bước 3: 'The prologue binds n_s, so a recorded noise1 never verifies on a new " + "channel')." +) + + +def build(ctx: dict) -> dict: + pairs = {p["name"]: p for p in ctx["pairs"]} + pv1, pv2 = pairs["cặp 1"], pairs["cặp 2"] + key_vectors = [_key_vector(pv1), _key_vector(pv2)] + disc = {v["name"]: H(v["k_bt_disc"]) for v in key_vectors} + link = {v["name"]: H(v["k_bt_link"]) for v in key_vectors} + hint_vectors = [_hint_vector(pv1, disc["cặp 1"])] + handshake = _handshake_vector(pv1, link["cặp 1"]) + negatives = _negatives(pv1, pv2, link["cặp 1"], link["cặp 2"], handshake) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Keys\", \"Advertisement\", \"Link-security handshake\"; PRK của " + "pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": key_vectors + hint_vectors + [handshake], "invalid_vectors": negatives} + return {"bt-link.json": doc} diff --git a/tools/vectors/build_bt_pairing_vectors.py b/tools/vectors/build_bt_pairing_vectors.py new file mode 100644 index 0000000..c167a70 --- /dev/null +++ b/tools/vectors/build_bt_pairing_vectors.py @@ -0,0 +1,104 @@ +"""Dựng bt-pairing.json: K_bt_pdisc/K_bt_plink, pr_bt (00-common-specs.md 0.4.5 "Keys", "Pairing mode") và một +vector "khung đầu" (first frame) — message 1 của Noise_NNpsk0_25519_ChaChaPoly_SHA256, psk = K_bt_plink — cho một +liên kết ghép đôi QR, cộng các mẫu âm của nó. + +Chưa có vector mode PIN (psk = K_bt_plink thay bằng K_bt_pinlink): cpace.json đã có ISK, file này chỉ là chưa +nối vào — xem ghi chú trong `description`. +""" +import bt_pairing_derivations as BT +import noise_nnpsk0 as N +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +PROLOGUE_LABEL = b"HLBT1" + + +def _key_vector(pv: dict) -> dict: + secret = H(pv["pairing_secret"]) + pdisc, plink = BT.k_bt_pdisc(secret), BT.k_bt_plink(secret) + mac = BT.hmac256(pdisc, BT.PR_BT_LABEL) + return {"name": pv["name"], "kind": "key", "pair_id": pv["pair_id"], "pairing_secret_source": "pair-prk.json", + "pairing_secret": secret.hex(), "k_bt_pdisc_info": BT.PDISC_INFO.decode(), "k_bt_pdisc": pdisc.hex(), + "k_bt_plink_info": BT.PLINK_INFO.decode(), "k_bt_plink": plink.hex(), + "pr_bt_label": BT.PR_BT_LABEL.decode(), "pr_bt_mac": mac.hex(), "pr_bt": mac[:4].hex()} + + +def _first_frame_vector(pv: dict, plink: bytes) -> dict: + n_s = test_bytes(f"bt first-frame n_s {pv['name']}", 32) + prologue = PROLOGUE_LABEL + n_s + e_priv = test_bytes(f"bt first-frame e_priv {pv['name']}", 32) + r = N.first_message(plink, prologue, e_priv) + opened = N.try_open_first_message(plink, prologue, r["e_pub"], r["ciphertext_tag"]) + assert opened == b"", f"{pv['name']}: khung đầu phải mở được với đúng psk" + kind, message1 = 0x02, r["message1"] + rlen = 1 + len(message1) + record = rlen.to_bytes(2, "big") + bytes([kind]) + message1 + return {"name": f"{pv['name']} / liên kết ghép đôi QR", "kind": "first_frame", "mode": "qr", + "pair_id": pv["pair_id"], "psk_source": f"k_bt_plink của {pv['name']}", "psk": plink.hex(), + "protocol_name": r["protocol_name"].decode(), "n_s": n_s.hex(), "prologue_label": PROLOGUE_LABEL.decode(), + "prologue": prologue.hex(), "h0": r["h0"].hex(), "h_after_prologue": r["h1"].hex(), + "mix_key_and_hash": {k: v.hex() for k, v in r["mix_key_and_hash"].items()}, + "h_after_psk": r["h2"].hex(), "ck_after_psk": r["ck1"].hex(), "k": r["k"].hex(), + "e_priv": r["e_priv"].hex(), "e_pub": r["e_pub"].hex(), "h_after_e": r["h3"].hex(), + "nonce": r["nonce"].hex(), "ciphertext_tag": r["ciphertext_tag"].hex(), "h_final": r["h4"].hex(), + "message1": message1.hex(), "record_kind": "0x02", "rlen": rlen, "record": record.hex()} + + +def _negatives(pv1: dict, pv2: dict, plink1: bytes, plink2: bytes, base: dict) -> list[dict]: + n_s, e_pub, ct = H(base["n_s"]), H(base["e_pub"]), H(base["ciphertext_tag"]) + prologue = H(base["prologue"]) + out = [] + + assert N.try_open_first_message(plink2, prologue, e_pub, ct) is None + out.append({"name": f"{pv1['name']} / S thử psk của {pv2['name']}", "reason": "wrong_key", + "n_s": base["n_s"], "e_pub": base["e_pub"], "ciphertext_tag": base["ciphertext_tag"], + "wrong_psk_source": f"k_bt_plink của {pv2['name']}", "wrong_psk": plink2.hex()}) + + bad_ct = bytearray(ct) + bad_ct[-1] ^= 0x01 + assert N.try_open_first_message(plink1, prologue, e_pub, bytes(bad_ct)) is None + out.append({"name": f"{pv1['name']} / tag sai", "reason": "tag_mismatch", "n_s": base["n_s"], + "e_pub": base["e_pub"], "ciphertext_tag": bytes(bad_ct).hex()}) + + bad_e = bytearray(e_pub) + bad_e[0] ^= 0x01 + assert N.try_open_first_message(plink1, prologue, bytes(bad_e), ct) is None + out.append({"name": f"{pv1['name']} / e_pub bị sửa trên đường truyền", "reason": "message_tampered", + "n_s": base["n_s"], "e_pub": bytes(bad_e).hex(), "ciphertext_tag": base["ciphertext_tag"]}) + + other_n_s = test_bytes(f"bt first-frame n_s khác kênh {pv1['name']}", 32) + assert N.try_open_first_message(plink1, PROLOGUE_LABEL + other_n_s, e_pub, ct) is None + out.append({"name": f"{pv1['name']} / phát lại message 1 trên kênh khác (n_s khác)", "reason": "aad_mismatch", + "n_s": other_n_s.hex(), "e_pub": base["e_pub"], "ciphertext_tag": base["ciphertext_tag"]}) + return out + + +DESCRIPTION = ( + "K_bt_pdisc = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-disc\", L 32); " + "K_bt_plink = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-link\", L 32); " + "pr_bt = 4 byte đầu của HMAC-SHA256(K_bt_pdisc, \"HLBTPAIR1\"), đổi theo mỗi QR, không suy ra được pk/ps " + "(0.4.5 \"Keys\", \"Pairing mode\"). kind \"first_frame\": message 1 (\"psk, e\") của " + "Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), psk = K_bt_plink, prologue = " + "\"HLBT1\" ‖ n_s — xác thực \"khung đầu\" của một liên kết ghép đôi trước khi slot pre-auth được cấp (0.4.5 " + "\"Pairing mode\", \"Link-security handshake\"). HKDF ở đây là HKDF nội bộ của Noise (3 ngõ ra qua HMAC), " + "khác HKDF-SHA256 RFC 5869 dùng ở các file khác trong kho này. `record` = rlen (uint16 BE) ‖ kind (0x02) ‖ " + "message1 — đúng khuôn \"Link records\" của 0.4.5. KHÔNG phải vector công bố của tài liệu Noise (tài liệu đó " + "không phát hành vector cho psk/prologue riêng của HandLive): đây là cách HandLive dùng đúng thuật toán của " + "tài liệu đó cho bộ psk/prologue của mình, cần Trang rà soát riêng. Chưa có vector mode PIN (psk thay bằng " + "K_bt_pinlink): cpace.json đã có ISK, chỉ là chưa nối vào, để lại cho một task sau." +) + + +def build(ctx: dict) -> dict: + pairs = {p["name"]: p for p in ctx["pairs"]} + pv1, pv2 = pairs["cặp 1"], pairs["cặp 2"] + key_vectors = [_key_vector(pv1), _key_vector(pv2)] + plink = {v["name"]: H(v["k_bt_plink"]) for v in key_vectors} + first_frame = _first_frame_vector(pv1, plink["cặp 1"]) + negatives = _negatives(pv1, pv2, plink["cặp 1"], plink["cặp 2"], first_frame) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Keys\", \"Pairing mode\", \"Link-security handshake\"; pairing_secret của " + "pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": key_vectors + [first_frame], "invalid_vectors": negatives} + return {"bt-pairing.json": doc} diff --git a/tools/vectors/build_cpace_vectors.py b/tools/vectors/build_cpace_vectors.py new file mode 100644 index 0000000..282bba8 --- /dev/null +++ b/tools/vectors/build_cpace_vectors.py @@ -0,0 +1,165 @@ +"""Dựng cpace.json: vector chính thức của draft-irtf-cfrg-cpace-21 (CPACE-X25519-SHA512, Appendix B.1), rồi các +case theo cách HandLive dùng (PIN qua Bluetooth không mạng, 00-common-specs.md 0.4.5 luồng C): `g`, `Ya`, `Yb`, +`K`, `ISK`, `K_bt_pinlink`, `K_bt_pin`; cộng mẫu âm (Ya/Yb bậc thấp, sid khác, PIN sai). +""" +import cpace_pin_derivations as PIN +import cpace_x25519_sha512 as C +from cpace_source_values import ( + CPACE_X25519_SHA512_GENERATOR as SRC_GEN, + CPACE_X25519_SHA512_LOW_ORDER_POINTS as SRC_LOW_POINTS, + CPACE_X25519_SHA512_MESSAGE as SRC_MSG, +) +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +DRAFT = "draft-irtf-cfrg-cpace-21" +HL_CI = b"HLBTPIN1" + + +def _official_generator_vector() -> dict: + prs, ci, sid = H(SRC_GEN["PRS"]), H(SRC_GEN["CI"]), H(SRC_GEN["sid"]) + g = C.calculate_generator(prs, ci, sid) + assert g == H(SRC_GEN["generator g"]), "calculate_generator phải khớp Appendix B.1.1 của draft" + return {"name": "draft B.1.1 calculate_generator", "kind": "official_generator", "draft": DRAFT, + "section": "Appendix B.1.1", "H": SRC_GEN["H"], "PRS": SRC_GEN["PRS"], "CI": SRC_GEN["CI"], + "sid": SRC_GEN["sid"], "generator_string": SRC_GEN["generator_string(G.DSI,PRS,CI,sid,H.s_in_bytes)"], + "hash_generator_string": SRC_GEN["hash generator string"], + "decoded_field_element": SRC_GEN["decoded field element of 255 bits"], "g": g.hex()} + + +def _official_message_vector() -> dict: + prs, ci, sid = H(SRC_MSG["PRS"]), H(SRC_MSG["CI"]), H(SRC_MSG["sid"]) + ya, ADa, yb, ADb = H(SRC_MSG["ya"]), H(SRC_MSG["ADa"]), H(SRC_MSG["yb"]), H(SRC_MSG["ADb"]) + r = C.run_both_sides(prs, ci, sid, ya, ADa, yb, ADb) + assert r["g"] == H(SRC_MSG["g"]) + assert r["Ya"] == H(SRC_MSG["Ya"]) and r["Yb"] == H(SRC_MSG["Yb"]) + assert r["k_a"] == H(SRC_MSG["K"]) == r["k_b"] + assert r["isk_a"] == H(SRC_MSG["ISK_IR"]) == r["isk_b"] + return {"name": "draft B.1.2-B.1.7 message A/B, K, ISK", "kind": "official_message", "draft": DRAFT, + "section": "Appendix B.1.2-B.1.7", "PRS": SRC_MSG["PRS"], "CI": SRC_MSG["CI"], "sid": SRC_MSG["sid"], + "g": r["g"].hex(), "ya": SRC_MSG["ya"], "ADa": SRC_MSG["ADa"], "Ya": r["Ya"].hex(), "yb": SRC_MSG["yb"], + "ADb": SRC_MSG["ADb"], "Yb": r["Yb"].hex(), "K": r["k_a"].hex(), "ISK_initiator_responder": r["isk_a"].hex(), + "ISK_parallel_source": SRC_MSG["ISK_SY"], + "note": "ISK_parallel_source là biến thể song hành (transcript_oc) công bố trong draft; HandLive chỉ " + "dùng khởi tạo/trả lời (transcript_ir, ISK_initiator_responder) vì C luôn khởi tạo và S luôn " + "trả lời (0.4.5 luồng C)."} + + +def _handlive_pin_vector(name: str, pin: str) -> dict: + prs = pin.encode("utf-8") + n_s = test_bytes(f"cpace pin n_s {name}", 32) + ya = test_bytes(f"cpace pin ya {name}", 32) + yb = test_bytes(f"cpace pin yb {name}", 32) + r = C.run_both_sides(prs, HL_CI, n_s, ya, b"", yb, b"") + assert r["k_a"] is not None and r["k_a"] == r["k_b"], f"{name}: K phải khớp cả hai phía" + assert r["isk_a"] == r["isk_b"] + isk = r["isk_a"] + pinlink, pinpair = PIN.k_bt_pinlink(isk, n_s), PIN.k_bt_pin(isk, n_s) + return {"name": name, "kind": "handlive_pin", "pin": pin, "ci": HL_CI.decode(), "n_s": n_s.hex(), + "ya": ya.hex(), "yb": yb.hex(), "g": r["g"].hex(), "Ya": r["Ya"].hex(), "Yb": r["Yb"].hex(), + "K": r["k_a"].hex(), "ISK": isk.hex(), "k_bt_pinlink_info": PIN.PINLINK_INFO.decode(), + "k_bt_pinlink": pinlink.hex(), "k_bt_pin_info": PIN.PIN_INFO.decode(), "k_bt_pin": pinpair.hex()} + + +def _negative_low_order(name: str, side: str) -> dict: + """side = "ya": C (khởi tạo) gửi Ya bậc thấp, S phải abort. side = "yb": S (trả lời) gửi Yb bậc thấp, C phải + abort. Dùng điểm u0 của draft (Appendix B.1.10, bất kể vô hướng — đã kiểm bằng vô hướng cố định của draft và + một vô hướng HandLive khác, cùng kết quả abort).""" + pin = "482917" + prs = pin.encode("utf-8") + n_s = test_bytes(f"cpace neg low_order {side} n_s", 32) + g = C.calculate_generator(prs, HL_CI, n_s) + low_u = H(SRC_LOW_POINTS[0]["u"]) # u0 = zero_bytes — luôn trong tập "MUST trigger abort" + assert SRC_LOW_POINTS[0]["must_abort"] + if side == "ya": + yb = test_bytes(f"cpace neg low_order {side} yb", 32) + Yb = C.scalar_mult_vfy(yb, g) + k_s = C.scalar_mult_vfy(yb, low_u) + assert k_s is None + return {"name": name, "reason": "low_order_ya", "pin": pin, "n_s": n_s.hex(), "g": g.hex(), + "Ya_attacker": low_u.hex(), "yb": yb.hex(), "Yb": Yb.hex(), + "expect": "S (trả lời) tính K = scalar_mult_vfy(yb, Ya) → None (G.I) → đóng kênh không trả lời"} + ya = test_bytes(f"cpace neg low_order {side} ya", 32) + Ya = C.scalar_mult_vfy(ya, g) + k_c = C.scalar_mult_vfy(ya, low_u) + assert k_c is None + return {"name": name, "reason": "low_order_yb", "pin": pin, "n_s": n_s.hex(), "g": g.hex(), "ya": ya.hex(), + "Ya": Ya.hex(), "Yb_attacker": low_u.hex(), + "expect": "C (khởi tạo) tính K = scalar_mult_vfy(ya, Yb) → None (G.I) → đóng kênh không trả lời"} + + +def _negative_sid_mismatch(name: str) -> dict: + pin = "482917" + prs = pin.encode("utf-8") + n_s_c = test_bytes("cpace neg sid_mismatch n_s_c", 32) + n_s_s = test_bytes("cpace neg sid_mismatch n_s_s", 32) + assert n_s_c != n_s_s + ya = test_bytes("cpace neg sid_mismatch ya", 32) + yb = test_bytes("cpace neg sid_mismatch yb", 32) + g_c, g_s = C.calculate_generator(prs, HL_CI, n_s_c), C.calculate_generator(prs, HL_CI, n_s_s) + Ya, Yb = C.scalar_mult_vfy(ya, g_c), C.scalar_mult_vfy(yb, g_s) + k_c, k_s = C.scalar_mult_vfy(ya, Yb), C.scalar_mult_vfy(yb, Ya) + isk_c = C.isk(n_s_c, k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = C.isk(n_s_s, k_s, Ya, b"", Yb, b"") if k_s else None + assert isk_c is not None and isk_s is not None and isk_c != isk_s, "sid khác nhau phải cho ISK khác nhau" + return {"name": name, "reason": "sid_mismatch", "pin": pin, "n_s_c": n_s_c.hex(), "n_s_s": n_s_s.hex(), + "ya": ya.hex(), "yb": yb.hex(), "Ya": Ya.hex(), "Yb": Yb.hex(), "isk_c": isk_c.hex(), + "isk_s": isk_s.hex(), + "expect": "C dùng sid của mình, S dùng sid của mình (hai kênh khác nhau trộn vào) → ISK hai phía " + "khác nhau (isk_c != isk_s), K_bt_pinlink suy từ đó cũng khác → noise1 của S không mở được"} + + +def _negative_wrong_pin(name: str) -> dict: + n_s = test_bytes("cpace neg wrong_pin n_s", 32) + pin_c, pin_s = "482917", "482918" + ya = test_bytes("cpace neg wrong_pin ya", 32) + yb = test_bytes("cpace neg wrong_pin yb", 32) + g_c = C.calculate_generator(pin_c.encode(), HL_CI, n_s) + g_s = C.calculate_generator(pin_s.encode(), HL_CI, n_s) + assert g_c != g_s, "PIN khác phải cho generator khác" + Ya, Yb = C.scalar_mult_vfy(ya, g_c), C.scalar_mult_vfy(yb, g_s) + k_c, k_s = C.scalar_mult_vfy(ya, Yb), C.scalar_mult_vfy(yb, Ya) + isk_c = C.isk(n_s, k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = C.isk(n_s, k_s, Ya, b"", Yb, b"") if k_s else None + assert isk_c is None or isk_s is None or isk_c != isk_s, "PIN khác phải cho ISK khác (hoặc abort)" + return {"name": name, "reason": "wrong_pin", "n_s": n_s.hex(), "pin_c": pin_c, "pin_s": pin_s, "ya": ya.hex(), + "yb": yb.hex(), "Ya": Ya.hex(), "Yb": Yb.hex(), "isk_c": isk_c.hex() if isk_c else None, + "isk_s": isk_s.hex() if isk_s else None, + "expect": "PIN gõ sai ở một phía → generator khác nhau → ISK hai phía khác nhau (hoặc K toàn 0) → " + "noise1 dùng K_bt_pinlink sai không mở được, một lần thử bị tính vào bộ đếm chung"} + + +DESCRIPTION = ( + "CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512). kind \"official_generator\"/" + "\"official_message\": vector công bố của draft (Appendix B.1), chép nguyên văn qua cpace_source_values.py, " + "dùng để kiểm cài đặt calculate_generator/Ya/Yb/K/ISK đúng thuật toán trước khi áp case của HandLive. kind " + "\"handlive_pin\": ghép đôi bằng PIN qua Bluetooth không mạng (0.4.5 luồng C) — PRS = UTF-8 của 6 chữ số PIN " + "(giữ số 0 đứng đầu), CI = \"HLBTPIN1\" cố định, sid = n_s (32 byte, mới cho mỗi kênh), ADa = ADb rỗng; C " + "khởi tạo (cpace1 = Ya), S trả lời (cpace2 = Yb); K_bt_pinlink/K_bt_pin = HKDF-SHA256(ikm = ISK, salt = n_s, " + "info tương ứng, L 32). invalid_vectors: low_order_ya/low_order_yb (bên kia nhận một điểm bậc thấp của " + "Appendix B.1.10 → K = G.I → phải đóng kênh không trả lời, đúng \"an all-zero K → close without answer\" của " + "0.4.5), sid_mismatch (hai phía dùng n_s khác nhau → ISK khác nhau), wrong_pin (PIN gõ sai một phía → " + "generator khác nhau → ISK khác nhau hoặc K toàn 0) — cả hai trường hợp sau không bị CPace tự abort, chỉ lộ " + "ra khi noise1 (psk = K_bt_pinlink) không mở được ở bước ghép đôi tiếp theo (bt-pairing.json, chưa nối case " + "PIN vào — còn một việc để làm sau)." +) + + +def build() -> dict: + official = [_official_generator_vector(), _official_message_vector()] + handlive = [ + _handlive_pin_vector("HandLive PIN 482917", "482917"), + _handlive_pin_vector("HandLive PIN với số 0 đứng đầu", "007042"), + ] + negatives = [ + _negative_low_order("Ya bậc thấp (C gửi điểm bậc thấp)", "ya"), + _negative_low_order("Yb bậc thấp (S trả lời điểm bậc thấp)", "yb"), + _negative_sid_mismatch("sid khác nhau giữa C và S"), + _negative_wrong_pin("PIN sai ở một phía"), + ] + doc = {"description": DESCRIPTION, + "source": f"{DRAFT} Appendix B.1 (CPACE-X25519-SHA512); {SPEC} 0.4.5 \"PIN (PAIR-01 flow C)\", " + "\"Keys from ISK\"", + "vectors": official + handlive, "invalid_vectors": negatives} + return {"cpace.json": doc} diff --git a/tools/vectors/cpace_pin_derivations.py b/tools/vectors/cpace_pin_derivations.py new file mode 100644 index 0000000..6b33672 --- /dev/null +++ b/tools/vectors/cpace_pin_derivations.py @@ -0,0 +1,16 @@ +"""K_bt_pinlink/K_bt_pin dẫn từ ISK của CPace (00-common-specs.md 0.4.5 "PIN", "Keys from ISK") — dùng để SINH +vector. `salt` = `n_s` (cũng là `sid` của CPace), không phải salt rỗng như K_bt_pdisc/K_bt_plink.""" +from handlive_protocol_derivations import hkdf + +PINLINK_INFO = b"handlive/v1/bt-pin-link" +PIN_INFO = b"handlive/v1/bt-pin-pair" + + +def k_bt_pinlink(isk: bytes, n_s: bytes) -> bytes: + """K_bt_pinlink = HKDF-SHA256(ikm = ISK, salt = n_s, info = "handlive/v1/bt-pin-link", L = 32).""" + return hkdf(isk, PINLINK_INFO, 32, salt=n_s) + + +def k_bt_pin(isk: bytes, n_s: bytes) -> bytes: + """K_bt_pin = HKDF-SHA256(ikm = ISK, salt = n_s, info = "handlive/v1/bt-pin-pair", L = 32).""" + return hkdf(isk, PIN_INFO, 32, salt=n_s) diff --git a/tools/vectors/cpace_source_values.py b/tools/vectors/cpace_source_values.py new file mode 100644 index 0000000..793be30 --- /dev/null +++ b/tools/vectors/cpace_source_values.py @@ -0,0 +1,87 @@ +"""Giá trị test vector CPace chép nguyên văn từ draft-irtf-cfrg-cpace-21 (tải bản .txt chính thức +từ IETF datatracker, 2026-10-28), phần Appendix B.1 "Test vector for CPace using group X25519 and +hash SHA-512". Trích tự động bằng script (giải base64 JSON nhúng sẵn trong draft ở B.1.1.1, B.1.9, +B.1.10.1 — không gõ tay) và đối chiếu khớp với bản hex-dump và bản C initializer cùng phần, cả ba +đều khớp byte-exact. Không sửa tay các giá trị này; verify_vectors.py kiểm lại bằng thư viện độc lập. +""" + +# B.1.1: calculate_generator(SHA-512, PRS, CI, sid) — generator_string, băm, phần tử trường, generator g. +CPACE_X25519_SHA512_GENERATOR = { + "H": "SHA-512", + "H.s_in_bytes": 128, + "PRS": "50617373776f7264", + "ZPAD length": 109, + "DSI": "4350616365323535", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "generator_string(G.DSI,PRS,CI,sid,H.s_in_bytes)": ( + "0843506163653235350850617373776f72646d0000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000180b415f696e69746961746f720b425f" + "726573706f6e646572107e4b4791d6a8ef019b936c79fb7f2c57" + ), + "hash generator string": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed1d3", + "decoded field element of 255 bits": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed153", + "generator g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f", +} + +# B.1.2-B.1.7: message A/B, K, ISK (initiator/responder và song hành), session id phụ. +CPACE_X25519_SHA512_MESSAGE = { + "PRS": "50617373776f7264", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f", + "ya": "21b4f4bd9e64ed355c3eb676a28ebedaf6d8f17bdc365995b319097153044080", + "ADa": "414461", + "Ya": "1d13c89278cdadd826f6d8d7f887701430f8380ddc17611cdd6dc989ce0c9f32", + "yb": "848b0779ff415f0af4ea14df9dd1d3c29ac41d836c7808896c4eba19c51ac40a", + "ADb": "414462", + "Yb": "248cccf6d5cdc3646f0ad593f9e6cef4e69d4945f8372e623512ecea32185623", + "K": "5b067effbdc0b2a0e1d907b21ebb25cfedb96a852179a847c37e43ee71322c6b", + "ISK_IR": ( + "6e19b875f7a561d6b3ca3dbb9ef42ac55de3e717881018204b8922b4d5e53bb2aa82c300bea7b65d2b671da71922ddf6" + "472301b79bc270adfa8bf413285f2263" + ), + "ISK_SY": ( + "eef745e2f6e7ae2b1a1e53da340e777167a07fe150436648c51fb199c11f3cbabfc683a2b48e1af5881940dc398d375c" + "95e6b4ae9948a45b8770de0656382be4" + ), + "sid_output_ir": ( + "cbc73f62589bbc96ab6a95ec2363df621e93bc3b0cea83ba6b9571d005fa8f5d2d08f7165622777fa484c02a9e6b20a8" + "4ee2dbebae8c53be757dcfc0eebdeb5f" + ), + "sid_output_oc": ( + "3a504e9c7f1f7fa7314861e2c487d13f28566f3043f0ca760d22c4911aca0dd8b1f12a7ad0862eb92d08a76120140412" + "ae6b8322e99d75cf1d20d8cfde2b40fe" + ), +} + +# B.1.10: G_X25519.scalar_mult_vfy trên điểm bậc thấp (low-order) — u0..ub với vô hướng s cố định; +# q0..qb là kết quả mong đợi (toàn 0 = phải abort). u0,u1,u2,u3,u4,u5,u7 PHẢI kích hoạt abort. +CPACE_X25519_SHA512_LOW_ORDER_SCALAR = "af46e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449aff" +CPACE_X25519_SHA512_LOW_ORDER_POINTS = [ + {"label": "u0", "u": "0000000000000000000000000000000000000000000000000000000000000000", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u1", "u": "0100000000000000000000000000000000000000000000000000000000000000", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u2", "u": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u3", "u": "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u4", "u": "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u5", "u": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u6", "u": "daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "q": "d8e2c776bbacd510d09fd9278b7edcd25fc5ae9adfba3b6e040e8d3b71b21806", "must_abort": False}, + {"label": "u7", "u": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u8", "u": "dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "q": "c85c655ebe8be44ba9c0ffde69f2fe10194458d137f09bbff725ce58803cdb38", "must_abort": False}, + {"label": "u9", "u": "d9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "q": "db64dafa9b8fdd136914e61461935fe92aa372cb056314e1231bc4ec12417456", "must_abort": False}, + {"label": "ua", "u": "cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880", + "q": "e062dcd5376d58297be2618c7498f55baa07d7e03184e8aada20bca28888bf7a", "must_abort": False}, + {"label": "ub", "u": "4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7", + "q": "993c6ad11c4c29da9a56f7691fd0ff8d732e49de6250b6c2e80003ff4629a175", "must_abort": False}, +] diff --git a/tools/vectors/cpace_x25519_sha512.py b/tools/vectors/cpace_x25519_sha512.py new file mode 100644 index 0000000..5893f8c --- /dev/null +++ b/tools/vectors/cpace_x25519_sha512.py @@ -0,0 +1,113 @@ +"""CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512): calculate_generator (Elligator2, +Appendix A.5), message A/B, K, ISK khởi tạo/trả lời (Appendix A.3.4, section 7.2). HandLive dùng suite này cho +ghép đôi bằng PIN qua Bluetooth không mạng (00-common-specs.md 0.4.5, luồng C): C khởi tạo (gửi `cpace1`), S trả +lời (gửi `cpace2`), nên chỉ cần `transcript_ir`/`isk` (khởi tạo/trả lời), không cần biến thể song hành (`_oc`). + +Kiểm end-to-end khớp byte-exact với vector B.1 của draft (`cpace_source_values.py`); `verify_vectors.py` kiểm lại +bằng pynacl (độc lập với `cryptography` dùng ở đây). +""" +import hashlib + +from cryptography.hazmat.primitives.asymmetric import x25519 + +DSI = b"CPace255" +DSI_ISK = DSI + b"_ISK" +H_S_IN_BYTES = 128 # kích thước khối nhập của SHA-512 +FIELD_SIZE_BYTES = 32 +FIELD_SIZE_BITS = 255 +Q_25519 = 2 ** 255 - 19 +A_25519 = 486662 +Z_25519 = 2 # find_z_ell2(GF(2^255 - 19)) cho Curve25519 (Appendix A.5) + + +def prepend_len(data: bytes) -> bytes: + """LEB128 độ dài ‖ data (Appendix A.1.1).""" + length, encoded = len(data), bytearray() + while True: + encoded.append(length if length < 128 else (length & 0x7F) + 0x80) + length >>= 7 + if length == 0: + break + return bytes(encoded) + data + + +def lv_cat(*parts: bytes) -> bytes: + """"length-value": nối các chuỗi, mỗi chuỗi có độ dài LEB128 đặt trước (Appendix A.1.3).""" + return b"".join(prepend_len(p) for p in parts) + + +def generator_string(prs: bytes, ci: bytes, sid: bytes, s_in_bytes: int = H_S_IN_BYTES, dsi: bytes = DSI) -> bytes: + """Appendix A.2: lv_cat(DSI, PRS, zero_bytes(len_zpad), CI, sid) — đệm 0 để DSI‖PRS lấp đầy khối băm đầu + tiên, làm độ dài băm độc lập với độ dài PRS thật (chống side-channel qua độ dài).""" + len_zpad = max(0, s_in_bytes - 1 - len(prepend_len(prs)) - len(prepend_len(dsi))) + return lv_cat(dsi, prs, bytes(len_zpad), ci, sid) + + +def decode_u_coordinate(u: bytes, bits: int = FIELD_SIZE_BITS) -> int: + """RFC 7748 decodeUCoordinate (Appendix A.4): xoá bit không dùng của byte cuối (mảng little-endian).""" + u_list = bytearray(u) + if bits % 8: + u_list[-1] &= (1 << (bits % 8)) - 1 + return int.from_bytes(bytes(u_list), "little") + + +def _encode_u_coordinate(u: int, bits: int = FIELD_SIZE_BITS) -> bytes: + return u.to_bytes((bits + 7) // 8, "little") + + +def elligator2_x25519(r: int) -> bytes: + """Appendix A.5 (map_to_curve_elligator2), A = 486662 và Z = 2 của Curve25519 — chuyển một phần tử trường r + (nguyên, đã decode_u_coordinate) thành toạ độ u hợp lệ của một điểm trên đường cong.""" + q, a = Q_25519, A_25519 % Q_25519 + z = Z_25519 % q + power = (q - 1) // 2 + denom = (1 + z * r * r) % q + v = (-a * pow(denom, q - 2, q)) % q + rhs = (pow(v, 3, q) + a * pow(v, 2, q) + v) % q + epsilon = pow(rhs, power, q) + if epsilon == q - 1: + epsilon = -1 + x = (epsilon * v - (1 - epsilon) * a * pow(2, q - 2, q)) % q + return _encode_u_coordinate(x) + + +def calculate_generator(prs: bytes, ci: bytes, sid: bytes) -> bytes: + """G.calculate_generator(SHA-512, PRS, CI, sid) của G_X25519 (0.4.5: `g` = calculate_generator(...)).""" + gen_str_hash = hashlib.sha512(generator_string(prs, ci, sid)).digest()[:FIELD_SIZE_BYTES] + return elligator2_x25519(decode_u_coordinate(gen_str_hash)) + + +def scalar_mult_vfy(scalar: bytes, point: bytes) -> bytes | None: + """G_X25519.scalar_mult_vfy = X25519(scalar, point); None (G.I — phải abort) khi `point` là điểm bậc thấp. + `cryptography` lên tiếng bằng ValueError đúng khi và chỉ khi kết quả là phần tử trung hoà (đã đối chiếu với + 12 điểm bậc thấp của Appendix B.1.10, bất kể vô hướng), nên bắt ValueError đúng với "một K toàn 0 → đóng kênh + không trả lời" (0.4.5, luồng C).""" + try: + priv = x25519.X25519PrivateKey.from_private_bytes(scalar) + pub = x25519.X25519PublicKey.from_public_bytes(point) + return priv.exchange(pub) + except ValueError: + return None + + +def transcript_ir(ya_pub: bytes, ada: bytes, yb_pub: bytes, adb: bytes) -> bytes: + """Appendix A.3.4 — thứ tự khởi tạo/trả lời cố định; HandLive luôn có vai rõ (C khởi tạo, S trả lời).""" + return lv_cat(ya_pub, ada) + lv_cat(yb_pub, adb) + + +def isk(sid: bytes, k: bytes, ya_pub: bytes, ada: bytes, yb_pub: bytes, adb: bytes) -> bytes: + """ISK = SHA-512(lv_cat(DSI ‖ "_ISK", sid, K) ‖ transcript_ir(Ya,ADa,Yb,ADb)) (section 7.2).""" + return hashlib.sha512(lv_cat(DSI_ISK, sid, k) + transcript_ir(ya_pub, ada, yb_pub, adb)).digest() + + +def run_both_sides(prs: bytes, ci: bytes, sid: bytes, ya: bytes, ada: bytes, yb: bytes, adb: bytes) -> dict: + """Chạy cả hai vai với đầu vào cho trước (dùng để sinh vector): trả g, Ya, Yb, k_a (phía C), k_b (phía S), + isk_a, isk_b. `k_a`/`k_b`/`isk_a`/`isk_b` là None khi vai đó phải abort (điểm nhận được bậc thấp).""" + g = calculate_generator(prs, ci, sid) + ya_pub = scalar_mult_vfy(ya, g) + yb_pub = scalar_mult_vfy(yb, g) + k_a = scalar_mult_vfy(ya, yb_pub) if ya_pub is not None and yb_pub is not None else None + k_b = scalar_mult_vfy(yb, ya_pub) if ya_pub is not None and yb_pub is not None else None + isk_a = isk(sid, k_a, ya_pub, ada, yb_pub, adb) if k_a is not None else None + isk_b = isk(sid, k_b, ya_pub, ada, yb_pub, adb) if k_b is not None else None + return {"g": g, "Ya": ya_pub, "Yb": yb_pub, "k_a": k_a, "k_b": k_b, "isk_a": isk_a, "isk_b": isk_b} diff --git a/tools/vectors/generate_vectors.py b/tools/vectors/generate_vectors.py index cea7d86..192fae1 100644 --- a/tools/vectors/generate_vectors.py +++ b/tools/vectors/generate_vectors.py @@ -10,7 +10,12 @@ sys.dont_write_bytecode = True # không để lại __pycache__ trong kho sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build_bt_advertisement_vectors # noqa: E402 +import build_bt_frame_vectors # noqa: E402 +import build_bt_link_vectors # noqa: E402 +import build_bt_pairing_vectors # noqa: E402 import build_clipboard_html_vectors # noqa: E402 +import build_cpace_vectors # noqa: E402 import build_discovery_hint_vectors # noqa: E402 import build_identity_session_vectors # noqa: E402 import build_message_vectors # noqa: E402 @@ -33,7 +38,12 @@ def render_all() -> dict[str, str]: files.update(build_revoke_vectors.build()) files.update(build_pair_handshake_vectors.build(ctx)) files.update(build_discovery_hint_vectors.build(ctx)) + files.update(build_bt_pairing_vectors.build(ctx)) + files.update(build_bt_link_vectors.build(ctx)) + files.update(build_bt_frame_vectors.build(ctx, files)) + files.update(build_bt_advertisement_vectors.build(ctx, files)) files.update(build_clipboard_html_vectors.build()) + files.update(build_cpace_vectors.build()) return {name: json.dumps(doc, indent=2, ensure_ascii=False) + "\n" for name, doc in sorted(files.items())} diff --git a/tools/vectors/noise_nnpsk0.py b/tools/vectors/noise_nnpsk0.py new file mode 100644 index 0000000..098e878 --- /dev/null +++ b/tools/vectors/noise_nnpsk0.py @@ -0,0 +1,139 @@ +"""Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34): message 1 ("psk, e") và message 2 +("e, ee") đủ cho cả "khung đầu" ghép đôi (00-common-specs.md 0.4.5 "Link-security handshake" bước 2–3, "Pairing +mode") và bắt tay bảo mật liên kết đầy đủ (bước 1–5, psk = `K_bt_link`, dùng cho các cặp đã ghép). HASHLEN = 32 +(SHA-256) nên temp_k/temp_k1/temp_k2 không bị cắt (quy tắc cắt chỉ áp dụng khi HASHLEN = 64). + +Rev. 34 §9.2 (chế độ psk): mỗi token "e" làm MixHash(e.pub) RỒI MixKey(e.pub) — không chỉ MixHash như pattern +không-psk. Thiếu bước MixKey(e.pub) này (lỗi #3418, bắt được qua kiểm chéo bằng thư viện `noiseprotocol` thật) +làm ck lan sai sang bước kế tiếp: message 1 mã nhầm bằng k của token "psk" thay vì k của token "e"; message 2 +tính "ee" trên ck sai nên ck2/Split() cũng sai theo, dù `e`/`e.pub` trên dây vẫn đúng (không đổi). + +Không phải vector công bố của tài liệu Noise — draft/tài liệu Noise không phát hành vector cho psk/prologue riêng +của HandLive. Đây là cách HandLive dùng đúng thuật toán của tài liệu đó (InitializeSymmetric, MixHash, +MixKeyAndHash, MixKey, EncryptAndHash, Split) cho bộ psk/prologue của mình, nên cần một vòng rà soát riêng (không +tự xác nhận được bằng vector bên ngoài) trước khi A7.1a/M7.1b khoá theo. +""" +import hashlib +import hmac as _hmac + +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + +from handlive_protocol_derivations import x25519_dh as _x25519_dh +from handlive_protocol_derivations import x25519_pub + +PROTOCOL_NAME = b"Noise_NNpsk0_25519_ChaChaPoly_SHA256" + + +def _hmac_sha256(key: bytes, msg: bytes) -> bytes: + return _hmac.new(key, msg, hashlib.sha256).digest() + + +def _sha256(data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + +def initialize_symmetric(protocol_name: bytes = PROTOCOL_NAME) -> tuple[bytes, bytes]: + """h = ck = protocol_name nếu ≤ 32 byte, ngược lại SHA-256(protocol_name) (ở đây 37 byte > 32). Trả (h, ck).""" + h = protocol_name if len(protocol_name) <= 32 else _sha256(protocol_name) + return h, h + + +def mix_key_and_hash(ck: bytes, h: bytes, ikm: bytes) -> dict: + """Token "psk": HKDF nội bộ của Noise (3 ngõ ra qua HMAC) rồi MixHash(temp_h); InitializeKey(temp_k).""" + temp_key = _hmac_sha256(ck, ikm) + out1 = _hmac_sha256(temp_key, b"\x01") + out2 = _hmac_sha256(temp_key, out1 + b"\x02") + out3 = _hmac_sha256(temp_key, out2 + b"\x03") + return {"temp_key": temp_key, "ck": out1, "temp_h": out2, "temp_k": out3, "h": _sha256(h + out2)} + + +def first_message(psk: bytes, prologue: bytes, e_priv: bytes) -> dict: + """Dựng message 1 ("psk, e") của C→S. Noise rev. 34 §9.2 (chế độ psk): mỗi token "e" làm MixHash(e.pub) RỒI + MixKey(e.pub) — khác pattern không-psk, nơi "e" chỉ MixHash. `k` dùng để mã hoá payload vì vậy là k SAU + MixKey(e.pub) của token "e", không phải k của token "psk" (bị đè ngay). `message1` = e.pub (32) ‖ + EncryptAndHash("") (16, chỉ có tag vì plaintext rỗng).""" + h0, ck0 = initialize_symmetric() + h1 = _sha256(h0 + prologue) # MixHash(prologue) + psk_step = mix_key_and_hash(ck0, h1, psk) # token "psk": MixKeyAndHash + h2, ck_psk = psk_step["h"], psk_step["ck"] + e_pub = x25519_pub(e_priv) + h3 = _sha256(h2 + e_pub) # MixHash(e.pub); khóa công khai tạm thời không bao giờ được mã hóa + e_step = mix_key(ck_psk, e_pub) # token "e", phần thêm của chế độ psk: MixKey(e.pub) + ck1, k = e_step["ck"], e_step["temp_k"] + nonce = bytes(4) + (0).to_bytes(8, "little") # n = 0 (lần ENCRYPT đầu tiên của hướng này) + ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h3) # EncryptAndHash(""), ad = h3 (MixKey không đụng h) + h4 = _sha256(h3 + ciphertext) # MixHash(ciphertext) + return {"protocol_name": PROTOCOL_NAME, "h0": h0, "prologue": prologue, "h1": h1, "psk": psk, + "mix_key_and_hash": psk_step, "h2": h2, "ck_after_psk": ck_psk, "mix_key_e": e_step, "ck1": ck1, "k": k, + "e_priv": e_priv, "e_pub": e_pub, "h3": h3, "nonce": nonce, "ciphertext_tag": ciphertext, "h4": h4, + "message1": e_pub + ciphertext} + + +def try_open_first_message(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext_tag: bytes) -> bytes | None: + """Phía S: thử một psk ứng viên. Trả b"" nếu tag khớp (xác thực được "khung đầu"), None nếu không — như + `reject`, không lộ lý do.""" + h0, ck0 = initialize_symmetric() + h1 = _sha256(h0 + prologue) + psk_step = mix_key_and_hash(ck0, h1, psk) + h2, ck_psk = psk_step["h"], psk_step["ck"] + h3 = _sha256(h2 + e_pub) + e_step = mix_key(ck_psk, e_pub) + k = e_step["temp_k"] + nonce = bytes(4) + (0).to_bytes(8, "little") + try: + return ChaCha20Poly1305(k).decrypt(nonce, ciphertext_tag, h3) + except Exception: + return None + + +def mix_key(ck: bytes, ikm: bytes) -> dict: + """Token "ee"/"es"/"se"/"ss": MixKey (2 ngõ ra qua HMAC, KHÔNG MixHash — khác token "psk").""" + temp_key = _hmac_sha256(ck, ikm) + out1 = _hmac_sha256(temp_key, b"\x01") + out2 = _hmac_sha256(temp_key, out1 + b"\x02") + return {"temp_key": temp_key, "ck": out1, "temp_k": out2} + + +def second_message(ck1: bytes, h_after_message1: bytes, e_priv_s: bytes, e_pub_c: bytes) -> dict: + """Dựng message 2 ("e, ee") của S→C, tiếp theo sau `first_message`. `ck1`/`h_after_message1` = `ck1`/`h4` của + `first_message`. Như message 1, token "e" ở chế độ psk làm MixHash RỒI MixKey(e.pub) trước khi token "ee" làm + MixKey(DH) — `k` dùng để mã hoá payload là k SAU "ee", vì "ee" đến sau và đè k của "e". `message2` = e.pub + của S (32) ‖ EncryptAndHash("") (16).""" + e_pub_s = x25519_pub(e_priv_s) + h5 = _sha256(h_after_message1 + e_pub_s) # MixHash(e.pub) của token "e" + e_step = mix_key(ck1, e_pub_s) # token "e", phần thêm của chế độ psk: MixKey(e.pub) + dh = _x25519_dh(e_priv_s, e_pub_c) + ee_step = mix_key(e_step["ck"], dh) # token "ee": MixKey(DH(e,re)) + ck2, k = ee_step["ck"], ee_step["temp_k"] + nonce = bytes(4) + (0).to_bytes(8, "little") # n = 0, k vừa được InitializeKey lại + ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h5) # ad = h5 (MixKey không đụng h) + h6 = _sha256(h5 + ciphertext) + return {"h5": h5, "mix_key_e": e_step, "dh": dh, "mix_key_ee": ee_step, "ck2": ck2, "k": k, "e_priv": e_priv_s, + "e_pub": e_pub_s, "nonce": nonce, "ciphertext_tag": ciphertext, "h6": h6, "message2": e_pub_s + ciphertext} + + +def try_open_second_message(ck1: bytes, h_after_message1: bytes, e_priv_c: bytes, e_pub_s: bytes, + ciphertext_tag: bytes) -> tuple[bytes | None, bytes | None, bytes | None]: + """Phía C: mở message 2. Trả (plaintext hoặc None, ck2, h6) — `ck2`/`h6` cần cho `split()` dù `plaintext` là + None (để caller tự quyết có đóng kênh hay không, không lộ gì qua số lượng giá trị trả về).""" + h5 = _sha256(h_after_message1 + e_pub_s) + e_step = mix_key(ck1, e_pub_s) + dh = _x25519_dh(e_priv_c, e_pub_s) + ee_step = mix_key(e_step["ck"], dh) + ck2, k = ee_step["ck"], ee_step["temp_k"] + nonce = bytes(4) + (0).to_bytes(8, "little") + h6 = _sha256(h5 + ciphertext_tag) + try: + pt = ChaCha20Poly1305(k).decrypt(nonce, ciphertext_tag, h5) + except Exception: + pt = None + return pt, ck2, h6 + + +def split(ck2: bytes) -> dict: + """Split(): 2 CipherState, một cho mỗi hướng. `c1` dùng để C mã hoá/S giải mã; `c2` ngược lại (Noise rev. 34 + §5, "Split").""" + temp_key = _hmac_sha256(ck2, b"") + out1 = _hmac_sha256(temp_key, b"\x01") + out2 = _hmac_sha256(temp_key, out1 + b"\x02") + return {"temp_key": temp_key, "c1": out1, "c2": out2} diff --git a/tools/vectors/requirements.txt b/tools/vectors/requirements.txt index d0ac723..783ab2c 100644 --- a/tools/vectors/requirements.txt +++ b/tools/vectors/requirements.txt @@ -2,3 +2,4 @@ cryptography==50.0.1 PyNaCl==1.6.2 argon2-cffi==25.1.0 argon2-cffi-bindings==26.1.0 +noiseprotocol==0.3.1 diff --git a/tools/vectors/verify_bt_advertisement_checks.py b/tools/vectors/verify_bt_advertisement_checks.py new file mode 100644 index 0000000..69eb1c2 --- /dev/null +++ b/tools/vectors/verify_bt_advertisement_checks.py @@ -0,0 +1,79 @@ +"""Kiểm bt-advertisement.json: dẫn lại K_bt_disc/hint bằng verify_common (hashlib/hmac, độc lập với +bt_link_derivations.py dùng `cryptography`); so khớp byte service_data và luật cửa sổ epoch bằng số học thuần. +""" +from verify_common import H, hkdf, hmac256 + +DISC_INFO = b"handlive/v1/bt-discovery" +HINT_LABEL = b"HLBTDISC1" +VER = 0x01 +BT_ADV_MAX_HINTS = 2 +BT_PIN_HINT = bytes(4) +REASONS = {"wrong_version", "hint_outside_window"} + + +def _hint(disc: bytes, epoch: int) -> bytes: + return hmac256(disc, HINT_LABEL + epoch.to_bytes(8, "big", signed=True))[:4] + + +def _service_data(psm: int, hints: list) -> bytes: + return bytes([VER]) + psm.to_bytes(2, "little") + b"".join(hints) + + +def _check_scan_response(c, n: str, v: dict) -> None: + c.eq(f"{n} ver", v["ver"], f"0x{VER:02x}") + c.true(f"{n} số hint ≤ BT_ADV_MAX_HINTS", len(v["hints"]) <= BT_ADV_MAX_HINTS) + data = _service_data(v["psm"], [H(h) for h in v["hints"]]) + c.eq(f"{n} service_data_bytes", v["service_data_bytes"], data.hex()) + c.eq(f"{n} service_data_len", v["service_data_len"], len(data)) + c.eq(f"{n} scan_response_total = 18 + len(service_data)", v["scan_response_total"], + v["ad_uuid_struct_bytes"] + len(data)) + + +def _check_pin_window(c, n: str, v: dict) -> None: + c.eq(f"{n} chỉ có BT_PIN_HINT", v["hints"], [BT_PIN_HINT.hex()]) + data = _service_data(v["psm"], [BT_PIN_HINT]) + c.eq(f"{n} service_data_bytes", v["service_data_bytes"], data.hex()) + + +def _check_negative(c, n: str, v: dict, disc: bytes) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason == "wrong_version": + data = H(v["service_data_bytes"]) + c.true(f"{n} byte đầu khác 0x01", data[0] != VER) + else: + advertised_hint = _hint(disc, v["advertised_epoch"]) + c.eq(f"{n} advertised_hint", v["advertised_hint"], advertised_hint.hex()) + diff = abs(v["advertised_epoch"] - v["client_epoch"]) + c.true(f"{n} kỷ nguyên cách client quá 4", diff > 4) + c.eq(f"{n} accepted_epochs = [client-4..client+4]", v["accepted_epochs"], + list(range(v["client_epoch"] - 4, v["client_epoch"] + 5))) + c.true(f"{n} advertised_epoch không nằm trong accepted_epochs", + v["advertised_epoch"] not in v["accepted_epochs"]) + + +def check_bt_advertisement(c, doc: dict, all_docs: dict) -> None: + link_doc = all_docs["bt-link.json"] + prk_doc = all_docs["pair-prk.json"] + pair1 = next(v for v in prk_doc["vectors"] if v["name"] == "cặp 1") + disc = hkdf(H(pair1["prk"]), DISC_INFO, 32) + key1 = next(v for v in link_doc["vectors"] if v["kind"] == "key" and v["name"] == "cặp 1") + c.eq("bt-advertisement: K_bt_disc khớp bt-link.json", disc.hex(), key1["k_bt_disc"]) + + kinds = set() + for v in doc["vectors"]: + n = f"bt-advertisement/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "scan_response": + _check_scan_response(c, n, v) + elif v["kind"] == "pin_window": + _check_pin_window(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-advertisement: có đủ loại vector", kinds, {"scan_response", "pin_window"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-advertisement/{v['name']}", v, disc) + c.eq("bt-advertisement: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-advertisement.json": check_bt_advertisement} diff --git a/tools/vectors/verify_bt_frame_checks.py b/tools/vectors/verify_bt_frame_checks.py new file mode 100644 index 0000000..d09da64 --- /dev/null +++ b/tools/vectors/verify_bt_frame_checks.py @@ -0,0 +1,107 @@ +"""Kiểm bt-frame.json bằng thư viện độc lập: ChaCha20-Poly1305 chéo `cryptography` + libsodium ietf (độc lập với +bt_frame_codec.py, vốn chỉ đóng gói byte không mã hoá); mọi luật cận (rlen, chan, more/long, BT_BULK_FRAGMENT_MAX, +BT_MESSAGE_MAX, BT_PREAUTH_RECORD_MAX) kiểm lại bằng số học thuần, không gọi lại bt_frame_codec.py. +""" +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from nacl.bindings import crypto_aead_chacha20poly1305_ietf_decrypt +from nacl.exceptions import CryptoError + +from verify_common import H + +RECORD_SIZES = {272, 1040, 4112} +CHAN_VALID = {0x01, 0x02} +BT_BULK_FRAGMENT_MAX = 1020 +BT_MESSAGE_MAX = 256 * 1024 +BT_PREAUTH_RECORD_MAX = 64 +REASONS = {"rlen_out_of_bounds", "preauth_record_over_bound", "unknown_chan", "more_without_long", + "bulk_fragment_over_max", "message_over_max"} + + +def _decrypt_both(key: bytes, nonce: bytes, ciphertext_tag: bytes): + try: + a = ChaCha20Poly1305(key).decrypt(nonce, ciphertext_tag, b"") + except InvalidTag: + a = None + try: + b = crypto_aead_chacha20poly1305_ietf_decrypt(ciphertext_tag, b"", nonce, key) + except CryptoError: + b = None + return a, b + + +def _parse_plaintext(plaintext: bytes) -> dict: + chan, flags = plaintext[0], plaintext[1] + dlen = int.from_bytes(plaintext[2:4], "big") + return {"chan": chan, "more": bool(flags & 0x01), "long": bool(flags & 0x02), "dlen": dlen, + "data": plaintext[4:4 + dlen], "padding": plaintext[4 + dlen:]} + + +def _check_transport_frame(c, n: str, v: dict, c1: bytes) -> None: + nonce = bytes(4) + v["counter"].to_bytes(8, "little") + c.eq(f"{n} nonce = 4 byte 0 ‖ counter LE", v["nonce"], nonce.hex()) + a, b = _decrypt_both(c1, nonce, H(v["ciphertext_tag"])) + c.eq(f"{n} giải mã được (cryptography)", a, H(v["plaintext"])) + c.eq(f"{n} giải mã được (libsodium)", b, H(v["plaintext"])) + parsed = _parse_plaintext(H(v["plaintext"])) + c.eq(f"{n} chan", int(v["chan"], 16), parsed["chan"]) + c.eq(f"{n} more", v["more"], parsed["more"]) + c.eq(f"{n} long", v["long"], parsed["long"]) + c.eq(f"{n} dlen", v["dlen"], parsed["dlen"]) + c.eq(f"{n} data", v["data"], parsed["data"].hex()) + c.true(f"{n} padding toàn 0", parsed["padding"] == bytes(len(parsed["padding"]))) + c.eq(f"{n} rlen đúng tập {{272,1040,4112}}", v["rlen"] in RECORD_SIZES, True) + c.eq(f"{n} record = rlen(2BE) ‖ ciphertext_tag", H(v["record"]), + v["rlen"].to_bytes(2, "big") + H(v["ciphertext_tag"])) + + +def _check_preauth_frame(c, n: str, v: dict) -> None: + body = H(v["body"]) + kind = int(v["preauth_kind"], 16) + c.eq(f"{n} byte đầu = kind", body[0], kind) + c.eq(f"{n} record = rlen(2BE) ‖ body[1:] với kind riêng", H(v["record"]), + len(body).to_bytes(2, "big") + body) + + +def _check_negative(c, n: str, v: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason == "rlen_out_of_bounds": + c.true(f"{n} rlen không thuộc {{272,1040,4112}}", v["rlen"] not in RECORD_SIZES) + c.eq(f"{n} record đúng rlen ghi trong vector", H(v["record"])[:2], v["rlen"].to_bytes(2, "big")) + elif reason == "preauth_record_over_bound": + c.true(f"{n} rlen > BT_PREAUTH_RECORD_MAX", v["rlen"] > BT_PREAUTH_RECORD_MAX) + elif reason == "unknown_chan": + c.true(f"{n} chan không thuộc {{0x01,0x02}}", int(v["chan"], 16) not in CHAN_VALID) + elif reason == "more_without_long": + parsed = _parse_plaintext(H(v["plaintext"])) + c.true(f"{n} more=1 và long=0", parsed["more"] and not parsed["long"]) + elif reason == "bulk_fragment_over_max": + c.true(f"{n} dlen > BT_BULK_FRAGMENT_MAX", v["dlen"] > BT_BULK_FRAGMENT_MAX) + else: # message_over_max + c.eq(f"{n} tổng sau mảnh này", v["total_after_fragment"], v["prior_bytes_received"] + v["last_fragment_dlen"]) + c.true(f"{n} tổng vượt BT_MESSAGE_MAX", v["total_after_fragment"] > BT_MESSAGE_MAX) + + +def check_bt_frame(c, doc: dict, all_docs: dict) -> None: + link_doc = all_docs["bt-link.json"] + handshake = next(v for v in link_doc["vectors"] if v["kind"] == "handshake") + c1 = H(handshake["c1"]) + kinds = set() + for v in doc["vectors"]: + n = f"bt-frame/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "transport_frame": + c.eq(f"{n} key_source là c1 của bt-link.json", v["key_source"], "c1 của bt-link.json") + _check_transport_frame(c, n, v, c1) + elif v["kind"] == "preauth_frame": + _check_preauth_frame(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-frame: có đủ loại vector", kinds, {"transport_frame", "preauth_frame"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-frame/{v['name']}", v) + c.eq("bt-frame: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-frame.json": check_bt_frame} diff --git a/tools/vectors/verify_bt_link_checks.py b/tools/vectors/verify_bt_link_checks.py new file mode 100644 index 0000000..48de9a6 --- /dev/null +++ b/tools/vectors/verify_bt_link_checks.py @@ -0,0 +1,183 @@ +"""Kiểm bt-link.json bằng thư viện độc lập với phía sinh: HKDF/HMAC qua verify_common (hashlib/hmac), X25519 qua +`x25519`/`x25519_base` (pynacl), ChaCha20Poly1305 kiểm chéo `cryptography` + libsodium ietf — độc lập với +noise_nnpsk0.py, giống verify_bt_pairing_checks.py nhưng thêm message 2 ("e, ee") và Split(). +""" +import hashlib +import hmac + +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from nacl.bindings import crypto_aead_chacha20poly1305_ietf_decrypt +from nacl.exceptions import CryptoError + +from verify_common import H, hkdf, hmac256, x25519, x25519_base + +DISC_INFO = b"handlive/v1/bt-discovery" +LINK_INFO = b"handlive/v1/bt-link" +HINT_LABEL = b"HLBTDISC1" +PROTOCOL_NAME = b"Noise_NNpsk0_25519_ChaChaPoly_SHA256" +PROLOGUE_LABEL = b"HLBT1" +REASONS = {"wrong_key", "tag_mismatch_noise1", "tag_mismatch_noise2", "replayed_channel"} +ZERO_NONCE = bytes(12) + + +def _sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() + + +def _hmac(key: bytes, msg: bytes) -> bytes: + return hmac.new(key, msg, hashlib.sha256).digest() + + +def _mix_key_and_hash(h: bytes, ck: bytes, ikm: bytes) -> tuple[bytes, bytes, bytes]: + """Token "psk": 3 ngõ ra qua HMAC (khác token "ee"/"e" ở chế độ psk, chỉ 2 ngõ ra) — trả (h mới, ck mới, k mới).""" + temp_key = _hmac(ck, ikm) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + out3 = _hmac(temp_key, out2 + b"\x03") + return _sha256(h + out2), out1, out3 + + +def _mix_key(ck: bytes, ikm: bytes) -> tuple[bytes, bytes]: + """Token "ee", hoặc token "e" ở chế độ psk (Noise rev. 34 §9.2, #3418): trả (ck mới, k mới), KHÔNG đụng h.""" + temp_key = _hmac(ck, ikm) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + return out1, out2 + + +def _open_chacha(k: bytes, aad: bytes, ciphertext_tag: bytes): + """Trả (mở bằng cryptography, mở bằng libsodium) — None khi tag không khớp.""" + try: + a = ChaCha20Poly1305(k).decrypt(ZERO_NONCE, ciphertext_tag, aad) + except InvalidTag: + a = None + try: + b = crypto_aead_chacha20poly1305_ietf_decrypt(ciphertext_tag, aad, ZERO_NONCE, k) + except CryptoError: + b = None + return a, b + + +def _open_message1(psk: bytes, prologue: bytes, e_pub_c: bytes, ciphertext_tag: bytes): + """Trả (mở cryptography, mở libsodium, h sau token "e" của C, ck sau token "e" — #3418: chế độ psk làm + MixHash(e.pub) RỒI MixKey(e.pub), nên k để mở tag KHÔNG phải k của token "psk").""" + h0 = _sha256(PROTOCOL_NAME) + h1 = _sha256(h0 + prologue) + h2, ck_psk, _ = _mix_key_and_hash(h1, h0, psk) + h3 = _sha256(h2 + e_pub_c) + ck1, k1 = _mix_key(ck_psk, e_pub_c) + a, b = _open_chacha(k1, h3, ciphertext_tag) + return a, b, h3, ck1 + + +def _open_message2(ck1: bytes, h_after_m1: bytes, e_priv_c: bytes, e_pub_s: bytes, ciphertext_tag: bytes): + """Trả (mở cryptography, mở libsodium, h sau token "e" của S, ck sau token "ee"). `ck1` phải là ck SAU token + "e" của message 1 (không phải ck của token "psk" một mình) — #3418.""" + h5 = _sha256(h_after_m1 + e_pub_s) + ck_e, _ = _mix_key(ck1, e_pub_s) # token "e" của S, phần thêm của chế độ psk + dh = x25519(e_priv_c, e_pub_s) + ck2, k2 = _mix_key(ck_e, dh) # token "ee" + a, b = _open_chacha(k2, h5, ciphertext_tag) + return a, b, h5, ck2 + + +def _split(ck2: bytes) -> tuple[bytes, bytes]: + temp_key = _hmac(ck2, b"") + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + return out1, out2 + + +def _check_key(c, n: str, v: dict, pair_prk: dict) -> bytes: + pv = pair_prk[v["name"]] + c.eq(f"{n} PRK lấy từ pair-prk.json", (v["prk_source"], v["prk"]), ("pair-prk.json", pv["prk"])) + prk = H(v["prk"]) + disc, link = hkdf(prk, DISC_INFO, 32), hkdf(prk, LINK_INFO, 32) + c.eq(f"{n} K_bt_disc", v["k_bt_disc"], disc.hex()) + c.eq(f"{n} K_bt_link", v["k_bt_link"], link.hex()) + return link + + +def _check_hint(c, n: str, v: dict, disc: dict) -> None: + disc_key = disc[v["k_bt_disc_source"]] + for e in v["epochs"]: + msg = HINT_LABEL + e["epoch"].to_bytes(8, "big", signed=True) + c.eq(f"{n} hint epoch {e['epoch']}", e["hint"], hmac256(disc_key, msg)[:4].hex()) + + +def _check_handshake(c, n: str, v: dict) -> None: + psk, prologue = H(v["psk"]), H(v["prologue"]) + c.eq(f"{n} prologue = nhãn ‖ n_s", prologue, PROLOGUE_LABEL + H(v["n_s"])) + challenge_body = bytes([0x01]) + H(v["n_s"]) + c.eq(f"{n} challenge_record = rlen(2BE) ‖ kind(0x01) ‖ n_s", H(v["challenge_record"]), + len(challenge_body).to_bytes(2, "big") + challenge_body) + c.eq(f"{n} e_pub_c = X25519 base point của e_priv_c", v["e_pub_c"], x25519_base(H(v["e_priv_c"])).hex()) + + a1, b1, h3, ck1 = _open_message1(psk, prologue, H(v["e_pub_c"]), H(v["ciphertext_tag_1"])) + c.eq(f"{n} h_after_e_c", h3.hex(), v["h_after_e_c"]) + c.eq(f"{n} ck_after_e_c", ck1.hex(), v["ck_after_e_c"]) + c.eq(f"{n} mở được noise1 (cryptography)", a1, b"") + c.eq(f"{n} mở được noise1 (libsodium)", b1, b"") + c.eq(f"{n} message1 = e_pub_c ‖ ciphertext_tag_1", v["message1"], v["e_pub_c"] + v["ciphertext_tag_1"]) + noise1_body = bytes([0x02]) + H(v["message1"]) + c.eq(f"{n} noise1_record = rlen(2BE) ‖ kind(0x02) ‖ message1", H(v["noise1_record"]), + len(noise1_body).to_bytes(2, "big") + noise1_body) + + c.eq(f"{n} e_pub_s = X25519 base point của e_priv_s", v["e_pub_s"], x25519_base(H(v["e_priv_s"])).hex()) + a2, b2, h5, ck2 = _open_message2(ck1, H(v["h_after_message1"]), H(v["e_priv_c"]), H(v["e_pub_s"]), + H(v["ciphertext_tag_2"])) + c.eq(f"{n} h_after_e_s", h5.hex(), v["h_after_e_s"]) + c.eq(f"{n} mở được noise2 (cryptography)", a2, b"") + c.eq(f"{n} mở được noise2 (libsodium)", b2, b"") + c.eq(f"{n} ck_after_ee", ck2.hex(), v["ck_after_ee"]) + c.eq(f"{n} message2 = e_pub_s ‖ ciphertext_tag_2", v["message2"], v["e_pub_s"] + v["ciphertext_tag_2"]) + noise2_body = bytes([0x03]) + H(v["message2"]) + c.eq(f"{n} noise2_record = rlen(2BE) ‖ kind(0x03) ‖ message2", H(v["noise2_record"]), + len(noise2_body).to_bytes(2, "big") + noise2_body) + + c1, c2 = _split(ck2) + c.eq(f"{n} Split() c1", v["c1"], c1.hex()) + c.eq(f"{n} Split() c2", v["c2"], c2.hex()) + c.true(f"{n} c1 != c2", c1 != c2) + + +def _check_negative(c, n: str, v: dict, hs: dict, link: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason == "tag_mismatch_noise2": + ck1 = H(hs["ck_after_e_c"]) + a, b, _, _ = _open_message2(ck1, H(hs["h_after_message1"]), H(hs["e_priv_c"]), H(v["e_pub_s"]), + H(v["ciphertext_tag_2"])) + c.eq(f"{n} phải bị từ chối (cả hai đường)", (a, b), (None, None)) + return + prologue = PROLOGUE_LABEL + H(v["n_s"]) + psk = H(v["wrong_psk"]) if reason == "wrong_key" else link["cặp 1"] + a, b, _, _ = _open_message1(psk, prologue, H(v["e_pub_c"]), H(v["ciphertext_tag_1"])) + c.eq(f"{n} phải bị từ chối (cả hai đường)", (a, b), (None, None)) + + +def check_bt_link(c, doc: dict, all_docs: dict) -> None: + pair_prk = {v["name"]: v for v in all_docs["pair-prk.json"]["vectors"]} + disc, link, kinds = {}, {}, set() + handshake = None + for v in doc["vectors"]: + n = f"bt-link/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "key": + disc[v["name"]] = H(v["k_bt_disc"]) + link[v["name"]] = _check_key(c, n, v, pair_prk) + elif v["kind"] == "hint": + _check_hint(c, n, v, disc) + elif v["kind"] == "handshake": + _check_handshake(c, n, v) + handshake = v + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-link: có đủ loại vector", kinds, {"key", "hint", "handshake"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-link/{v['name']}", v, handshake, link) + c.eq("bt-link: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-link.json": check_bt_link} diff --git a/tools/vectors/verify_bt_pairing_checks.py b/tools/vectors/verify_bt_pairing_checks.py new file mode 100644 index 0000000..d538db9 --- /dev/null +++ b/tools/vectors/verify_bt_pairing_checks.py @@ -0,0 +1,126 @@ +"""Kiểm bt-pairing.json bằng thư viện độc lập: HKDF/HMAC qua verify_common (hashlib/hmac), X25519 qua +`x25519_base` (pynacl), ChaCha20Poly1305 kiểm chéo `cryptography` + libsodium ietf — độc lập với +noise_nnpsk0.py (phía sinh). +""" +import hashlib +import hmac + +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from nacl.bindings import crypto_aead_chacha20poly1305_ietf_decrypt +from nacl.exceptions import CryptoError + +from verify_common import H, hkdf, hmac256, x25519_base + +PDISC_INFO = b"handlive/v1/bt-pair-disc" +PLINK_INFO = b"handlive/v1/bt-pair-link" +PR_BT_LABEL = b"HLBTPAIR1" +PROTOCOL_NAME = b"Noise_NNpsk0_25519_ChaChaPoly_SHA256" +PROLOGUE_LABEL = b"HLBT1" +REASONS = {"wrong_key", "tag_mismatch", "message_tampered", "aad_mismatch"} +ZERO_NONCE = bytes(12) + + +def _sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() + + +def _hmac(key: bytes, msg: bytes) -> bytes: + return hmac.new(key, msg, hashlib.sha256).digest() + + +def _ck_h_after_psk(prologue: bytes, psk: bytes) -> tuple[bytes, bytes]: + """InitializeSymmetric → MixHash(prologue) → token "psk" (MixKeyAndHash). Trả (ck, h) sau bước này.""" + h0 = _sha256(PROTOCOL_NAME) # 37 byte > HASHLEN 32 + h1 = _sha256(h0 + prologue) + temp_key = _hmac(h0, psk) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + return out1, _sha256(h1 + out2) + + +def _mix_key(ck: bytes, ikm: bytes) -> tuple[bytes, bytes]: + """Token "ee"/"e" (chế độ psk, Noise rev. 34 §9.2): MixKey, 2 ngõ ra qua HMAC — KHÔNG MixHash.""" + temp_key = _hmac(ck, ikm) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + return out1, out2 + + +def _open(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext_tag: bytes): + """Trả (mở bằng cryptography, mở bằng libsodium, h sau token "e") — None khi tag không khớp. Chế độ psk: token + "e" làm MixHash(e.pub) RỒI MixKey(e.pub) (#3418) — k dùng để mở tag là k SAU MixKey(e.pub), không phải k của + token "psk".""" + ck, h = _ck_h_after_psk(prologue, psk) + h3 = _sha256(h + e_pub) + _, k = _mix_key(ck, e_pub) + try: + a = ChaCha20Poly1305(k).decrypt(ZERO_NONCE, ciphertext_tag, h3) + except InvalidTag: + a = None + try: + b = crypto_aead_chacha20poly1305_ietf_decrypt(ciphertext_tag, h3, ZERO_NONCE, k) + except CryptoError: + b = None + return a, b, h3 + + +def _check_key(c, n: str, v: dict, pair_prk: dict) -> bytes: + pv = pair_prk[v["name"]] + c.eq(f"{n} pairing_secret lấy từ pair-prk.json", (v["pairing_secret_source"], v["pairing_secret"]), + ("pair-prk.json", pv["pairing_secret"])) + secret = H(v["pairing_secret"]) + pdisc = hkdf(secret, PDISC_INFO, 32) + plink = hkdf(secret, PLINK_INFO, 32) + c.eq(f"{n} K_bt_pdisc", v["k_bt_pdisc"], pdisc.hex()) + c.eq(f"{n} K_bt_plink", v["k_bt_plink"], plink.hex()) + mac = hmac256(pdisc, PR_BT_LABEL) + c.eq(f"{n} pr_bt_mac", v["pr_bt_mac"], mac.hex()) + c.eq(f"{n} pr_bt = 4 byte đầu của pr_bt_mac", v["pr_bt"], mac[:4].hex()) + return plink + + +def _check_first_frame(c, n: str, v: dict) -> None: + psk, prologue = H(v["psk"]), H(v["prologue"]) + c.eq(f"{n} prologue = nhãn ‖ n_s", prologue, PROLOGUE_LABEL + H(v["n_s"])) + c.eq(f"{n} e_pub = X25519 base point của e_priv", v["e_pub"], x25519_base(H(v["e_priv"])).hex()) + a, b, h3 = _open(psk, prologue, H(v["e_pub"]), H(v["ciphertext_tag"])) + c.eq(f"{n} h_after_e", h3.hex(), v["h_after_e"]) + c.eq(f"{n} mở được bằng cryptography (đúng psk)", a, b"") + c.eq(f"{n} mở được bằng libsodium (đúng psk)", b, b"") + c.eq(f"{n} message1 = e_pub ‖ ciphertext_tag", v["message1"], v["e_pub"] + v["ciphertext_tag"]) + rlen = 1 + len(H(v["message1"])) + c.eq(f"{n} rlen = 1 (kind) + độ dài message1", v["rlen"], rlen) + c.eq(f"{n} record = rlen(2 BE) ‖ kind(0x02) ‖ message1", H(v["record"]), + rlen.to_bytes(2, "big") + bytes([0x02]) + H(v["message1"])) + + +def _check_negative(c, n: str, v: dict, plink: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + prologue = PROLOGUE_LABEL + H(v["n_s"]) + psk = H(v["wrong_psk"]) if reason == "wrong_key" else plink["cặp 1"] + a, b, _ = _open(psk, prologue, H(v["e_pub"]), H(v["ciphertext_tag"])) + c.eq(f"{n} phải bị từ chối (cả hai đường)", (a, b), (None, None)) + + +def check_bt_pairing(c, doc: dict, all_docs: dict) -> None: + pair_prk = {v["name"]: v for v in all_docs["pair-prk.json"]["vectors"]} + plink, kinds = {}, set() + for v in doc["vectors"]: + n = f"bt-pairing/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "key": + plink[v["name"]] = _check_key(c, n, v, pair_prk) + elif v["kind"] == "first_frame": + _check_first_frame(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-pairing: có cả hai kind", kinds, {"key", "first_frame"}) + c.eq("bt-pairing: mỗi cặp của pair-prk.json có một vector key", sorted(plink), sorted(pair_prk)) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-pairing/{v['name']}", v, plink) + c.eq("bt-pairing: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-pairing.json": check_bt_pairing} diff --git a/tools/vectors/verify_cpace_checks.py b/tools/vectors/verify_cpace_checks.py new file mode 100644 index 0000000..9bc8f5b --- /dev/null +++ b/tools/vectors/verify_cpace_checks.py @@ -0,0 +1,142 @@ +"""Kiểm cpace.json bằng thư viện độc lập. `calculate_generator`/`generator_string` (Elligator2) dùng lại +`cpace_x25519_sha512.py` vì không có thư viện khác cho map_to_curve_elligator2 — giống cách `hchacha20_reference.py` +dùng chung cho HChaCha20 (verify_common.py); đã đối chiếu byte-exact với Appendix B.1.1 của draft trong +`build_cpace_vectors.py`. X25519/scalar_mult_vfy ở đây qua pynacl (phía sinh dùng `cryptography`); lv_cat, +transcript, ISK, HKDF cài lại bằng hashlib/hmac, độc lập với `cpace_x25519_sha512.py` và +`handlive_protocol_derivations.py`. +""" +import hashlib + +from nacl.bindings import crypto_scalarmult +from nacl.exceptions import CryptoError + +from cpace_x25519_sha512 import calculate_generator +from verify_common import H, hkdf + +DSI_ISK = b"CPace255_ISK" +REASONS = {"low_order_ya", "low_order_yb", "sid_mismatch", "wrong_pin"} + + +def _prepend_len(data: bytes) -> bytes: + length, out = len(data), bytearray() + while True: + out.append(length if length < 128 else (length & 0x7F) + 0x80) + length >>= 7 + if length == 0: + break + return bytes(out) + data + + +def _lv_cat(*parts: bytes) -> bytes: + return b"".join(_prepend_len(p) for p in parts) + + +def _transcript_ir(ya: bytes, ada: bytes, yb: bytes, adb: bytes) -> bytes: + return _lv_cat(ya, ada) + _lv_cat(yb, adb) + + +def _isk(sid: bytes, k: bytes, ya: bytes, ada: bytes, yb: bytes, adb: bytes) -> bytes: + return hashlib.sha512(_lv_cat(DSI_ISK, sid, k) + _transcript_ir(ya, ada, yb, adb)).digest() + + +def _scalar_mult_vfy(scalar: bytes, point: bytes) -> bytes | None: + try: + return crypto_scalarmult(scalar, point) + except CryptoError: + return None + + +def _check_official_generator(c, n: str, v: dict) -> None: + g = calculate_generator(H(v["PRS"]), H(v["CI"]), H(v["sid"])) + c.eq(f"{n} g (Elligator2)", v["g"], g.hex()) + + +def _check_official_message(c, n: str, v: dict) -> None: + g = calculate_generator(H(v["PRS"]), H(v["CI"]), H(v["sid"])) + c.eq(f"{n} g", v["g"], g.hex()) + Ya = _scalar_mult_vfy(H(v["ya"]), g) + Yb = _scalar_mult_vfy(H(v["yb"]), g) + c.eq(f"{n} Ya = scalar_mult_vfy(ya, g)", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb = scalar_mult_vfy(yb, g)", v["Yb"], Yb.hex() if Yb else None) + k_a = _scalar_mult_vfy(H(v["ya"]), Yb) + k_b = _scalar_mult_vfy(H(v["yb"]), Ya) + c.eq(f"{n} K khớp cả hai phía", (v["K"], v["K"]), (k_a.hex() if k_a else None, k_b.hex() if k_b else None)) + isk = _isk(H(v["sid"]), H(v["K"]), Ya, H(v["ADa"]), Yb, H(v["ADb"])) + c.eq(f"{n} ISK (khởi tạo/trả lời)", v["ISK_initiator_responder"], isk.hex()) + + +def _check_handlive_pin(c, n: str, v: dict) -> None: + prs, ci, n_s = v["pin"].encode("utf-8"), v["ci"].encode("utf-8"), H(v["n_s"]) + c.eq(f"{n} ci = HLBTPIN1 (0.4.5 luồng C)", v["ci"], "HLBTPIN1") + g = calculate_generator(prs, ci, n_s) + c.eq(f"{n} g", v["g"], g.hex()) + Ya = _scalar_mult_vfy(H(v["ya"]), g) + Yb = _scalar_mult_vfy(H(v["yb"]), g) + c.eq(f"{n} Ya", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb", v["Yb"], Yb.hex() if Yb else None) + k_a = _scalar_mult_vfy(H(v["ya"]), Yb) + c.eq(f"{n} K", v["K"], k_a.hex() if k_a else None) + isk = _isk(n_s, H(v["K"]), Ya, b"", Yb, b"") + c.eq(f"{n} ISK", v["ISK"], isk.hex()) + c.eq(f"{n} K_bt_pinlink", v["k_bt_pinlink"], hkdf(isk, v["k_bt_pinlink_info"].encode(), 32, salt=n_s).hex()) + c.eq(f"{n} K_bt_pin", v["k_bt_pin"], hkdf(isk, v["k_bt_pin_info"].encode(), 32, salt=n_s).hex()) + + +def _check_negative(c, n: str, v: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason in ("low_order_ya", "low_order_yb"): + g = calculate_generator(v["pin"].encode("utf-8"), b"HLBTPIN1", H(v["n_s"])) + c.eq(f"{n} g", v["g"], g.hex()) + if reason == "low_order_ya": + k_s = _scalar_mult_vfy(H(v["yb"]), H(v["Ya_attacker"])) + else: + k_s = _scalar_mult_vfy(H(v["ya"]), H(v["Yb_attacker"])) + c.eq(f"{n} K phải là None (G.I, điểm bậc thấp)", k_s, None) + elif reason == "sid_mismatch": + g_c = calculate_generator(v["pin"].encode("utf-8"), b"HLBTPIN1", H(v["n_s_c"])) + g_s = calculate_generator(v["pin"].encode("utf-8"), b"HLBTPIN1", H(v["n_s_s"])) + Ya, Yb = _scalar_mult_vfy(H(v["ya"]), g_c), _scalar_mult_vfy(H(v["yb"]), g_s) + c.eq(f"{n} Ya", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb", v["Yb"], Yb.hex() if Yb else None) + k_c, k_s = _scalar_mult_vfy(H(v["ya"]), Yb), _scalar_mult_vfy(H(v["yb"]), Ya) + isk_c = _isk(H(v["n_s_c"]), k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = _isk(H(v["n_s_s"]), k_s, Ya, b"", Yb, b"") if k_s else None + c.eq(f"{n} isk_c", v["isk_c"], isk_c.hex() if isk_c else None) + c.eq(f"{n} isk_s", v["isk_s"], isk_s.hex() if isk_s else None) + c.true(f"{n} isk_c phải khác isk_s", isk_c != isk_s) + else: # wrong_pin + g_c = calculate_generator(v["pin_c"].encode("utf-8"), b"HLBTPIN1", H(v["n_s"])) + g_s = calculate_generator(v["pin_s"].encode("utf-8"), b"HLBTPIN1", H(v["n_s"])) + c.true(f"{n} generator hai phía phải khác (PIN khác)", g_c != g_s) + Ya, Yb = _scalar_mult_vfy(H(v["ya"]), g_c), _scalar_mult_vfy(H(v["yb"]), g_s) + c.eq(f"{n} Ya", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb", v["Yb"], Yb.hex() if Yb else None) + k_c, k_s = _scalar_mult_vfy(H(v["ya"]), Yb), _scalar_mult_vfy(H(v["yb"]), Ya) + isk_c = _isk(H(v["n_s"]), k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = _isk(H(v["n_s"]), k_s, Ya, b"", Yb, b"") if k_s else None + c.eq(f"{n} isk_c", v["isk_c"], isk_c.hex() if isk_c else None) + c.eq(f"{n} isk_s", v["isk_s"], isk_s.hex() if isk_s else None) + c.true(f"{n} isk_c phải khác isk_s (hoặc một phía abort)", isk_c is None or isk_s is None or isk_c != isk_s) + + +def check_cpace(c, doc: dict) -> None: + kinds = set() + for v in doc["vectors"]: + n = f"cpace/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "official_generator": + _check_official_generator(c, n, v) + elif v["kind"] == "official_message": + _check_official_message(c, n, v) + elif v["kind"] == "handlive_pin": + _check_handlive_pin(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("cpace: có đủ loại vector", kinds, {"official_generator", "official_message", "handlive_pin"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"cpace/{v['name']}", v) + c.eq("cpace: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"cpace.json": check_cpace} diff --git a/tools/vectors/verify_noise_library_checks.py b/tools/vectors/verify_noise_library_checks.py new file mode 100644 index 0000000..8d955f3 --- /dev/null +++ b/tools/vectors/verify_noise_library_checks.py @@ -0,0 +1,56 @@ +"""Kiểm chéo bắt tay Noise_NNpsk0_25519_ChaChaPoly_SHA256 trên vector `handshake` của bt-link.json bằng +`noiseprotocol` (PyPI, pin ở tools/vectors/requirements.txt) — thư viện Noise ĐỘC LẬP HOÀN TOÀN với +noise_nnpsk0.py (không chỉ đổi primitive như verify_bt_link_checks.py, mà chạy lại chính thuật toán Noise bằng +mã người khác viết), theo ghi chú của Trang ở #3328 và yêu cầu của Lead ở #3376/#3380. Đã tự chạy thật (Lead cài +`noiseprotocol` qua `setup-cpace-refs.sh`, #3412): bắt đúng lỗi thiếu MixKey(e.pub) của chế độ psk (#3418), sau +khi vá noise_nnpsk0.py thì `message1`, `message2`, `handshake_hash` và cả hai khoá `Split()` (`cipher_state_encrypt.k`/ +`cipher_state_decrypt.k`) đều khớp byte-exact với `noiseprotocol`. + +THEO YÊU CẦU CỦA LEAD: thiếu `noiseprotocol` phải báo lỗi rõ, không âm thầm bỏ qua — môi trường chưa chạy +`pip install -r requirements.txt` (có `noiseprotocol==0.3.1`) sẽ thấy lỗi ImportError ở đây, không phải một dòng +"bỏ qua" lặng lẽ. +""" +from verify_common import H + +PROTOCOL_NAME = "Noise_NNpsk0_25519_ChaChaPoly_SHA256" + + +def _side(role: str, psk: bytes, prologue: bytes, e_priv: bytes): + from noise.connection import Keypair, NoiseConnection # import trong hàm: báo lỗi rõ đúng tại bước này, + # không chặn mọi phép kiểm khác của verify_vectors.py khi thiếu thư viện (xem lời gọi trong verify_vectors.py). + conn = NoiseConnection.from_name(PROTOCOL_NAME.encode()) + conn.set_as_initiator() if role == "initiator" else conn.set_as_responder() + conn.set_psks(psk) + conn.set_prologue(prologue) + conn.set_keypair_from_private_bytes(Keypair.EPHEMERAL, e_priv) + conn.start_handshake() + return conn + + +def check_bt_link_with_noiseprotocol(c, doc: dict, all_docs: dict) -> None: + handshake = next(v for v in doc["vectors"] if v["kind"] == "handshake") + n = f"bt-link/noiseprotocol/{handshake['name']}" + psk, prologue = H(handshake["psk"]), H(handshake["prologue"]) + e_priv_c, e_priv_s = H(handshake["e_priv_c"]), H(handshake["e_priv_s"]) + + initiator = _side("initiator", psk, prologue, e_priv_c) + responder = _side("responder", psk, prologue, e_priv_s) + + message1 = initiator.write_message(b"") + c.eq(f"{n} message1 (noiseprotocol khởi tạo)", message1, H(handshake["message1"])) + responder.read_message(message1) + + message2 = responder.write_message(b"") + c.eq(f"{n} message2 (noiseprotocol trả lời)", message2, H(handshake["message2"])) + initiator.read_message(message2) + + c.true(f"{n} cả hai phía xong bắt tay", initiator.handshake_finished and responder.handshake_finished) + c.eq(f"{n} handshake_hash (h6) cả hai phía", initiator.noise_protocol.handshake_hash, + responder.noise_protocol.handshake_hash) + c.eq(f"{n} handshake_hash (h6) khớp vector", initiator.noise_protocol.handshake_hash, H(handshake["h_final"])) + c.eq(f"{n} Split() c1 (C mã hoá/S giải mã)", initiator.noise_protocol.cipher_state_encrypt.k, + H(handshake["c1"])) + c.eq(f"{n} Split() c1 khớp phía S", responder.noise_protocol.cipher_state_decrypt.k, H(handshake["c1"])) + c.eq(f"{n} Split() c2 (S mã hoá/C giải mã)", responder.noise_protocol.cipher_state_encrypt.k, + H(handshake["c2"])) + c.eq(f"{n} Split() c2 khớp phía C", initiator.noise_protocol.cipher_state_decrypt.k, H(handshake["c2"])) diff --git a/tools/vectors/verify_vectors.py b/tools/vectors/verify_vectors.py index bb7232a..92ec032 100644 --- a/tools/vectors/verify_vectors.py +++ b/tools/vectors/verify_vectors.py @@ -11,9 +11,15 @@ sys.dont_write_bytecode = True # không để lại __pycache__ trong kho sys.path.insert(0, str(Path(__file__).resolve().parent)) +import verify_bt_advertisement_checks # noqa: E402 +import verify_bt_frame_checks # noqa: E402 +import verify_bt_link_checks # noqa: E402 +import verify_bt_pairing_checks # noqa: E402 import verify_clipboard_html_checks # noqa: E402 +import verify_cpace_checks # noqa: E402 import verify_discovery_hint_checks # noqa: E402 import verify_message_checks # noqa: E402 +import verify_noise_library_checks # noqa: E402 import verify_pair_handshake_checks # noqa: E402 import verify_primitive_checks # noqa: E402 import verify_push_relay_checks # noqa: E402 @@ -31,7 +37,9 @@ def main() -> int: checks = {**verify_primitive_checks.CHECKS, **verify_session_checks.CHECKS, **verify_message_checks.CHECKS, **verify_signature_checks.CHECKS, **verify_pair_handshake_checks.CHECKS, **verify_discovery_hint_checks.CHECKS, **verify_push_relay_checks.CHECKS, - **verify_revoke_checks.CHECKS} + **verify_revoke_checks.CHECKS, **verify_bt_pairing_checks.CHECKS, **verify_cpace_checks.CHECKS, + **verify_bt_link_checks.CHECKS, **verify_bt_frame_checks.CHECKS, + **verify_bt_advertisement_checks.CHECKS} c = Checker() for name in checks: c.true(f"{name} tồn tại", name in docs) @@ -60,6 +68,16 @@ def main() -> int: c.failures.append(f"clipboard-html.json: ngoại lệ {type(exc).__name__}: {exc}") print(f"{'OK ' if len(c.failures) == before[1] else 'SAI'} {verify_clipboard_html_checks.NAME:26s} " f"{len(html_doc['cases']):2d} case, {c.count - before[0]:3d} phép kiểm") + link_doc, before = docs.get("bt-link.json"), (c.count, len(c.failures)) + if link_doc is not None: + try: + verify_noise_library_checks.check_bt_link_with_noiseprotocol(c, link_doc, docs) + status = "OK " if len(c.failures) == before[1] else "SAI" + except ImportError as exc: + c.failures.append(f"bt-link.json/noiseprotocol: thiếu thư viện độc lập (pip install -r " + f"tools/vectors/requirements.txt, có noiseprotocol==0.3.1) — {exc}") + status = "LỖI (thiếu thư viện)" + print(f"{status} bt-link.json/noiseprotocol {c.count - before[0]:3d} phép kiểm") for f in c.failures: print(f" LỖI: {f}") print(f"Tổng: {c.count} phép kiểm, {len(c.failures)} lỗi")