From c4b09b37e2564d6d3f9c47f8d9fba6a06febd24f Mon Sep 17 00:00:00 2001 From: Dmitrii Fedorov Date: Mon, 19 Oct 2026 00:48:37 +0700 Subject: [PATCH] spec(port): land the remaining CR-CHAT ring batch -- 18 targets (Closes #10579) One ring-batch commit for the 18 port targets of CR-CHAT-03 (tracker #8883) and CR-CHAT-04 wave 2 (tracker #8884), as specs/policy/pr_budget.t27 asks: the specs, their seals, 18 t27b ledger rows and one duplicate-body bless. Squashed from the branch's commits onto master so every commit carries its issue (L1): - spec(port): land the remaining CR-CHAT ring batch -- 18 targets - ci(dupe): bless the batch's deliberate helper copies - port(trios-chat): give the three duplicate type names a unique name (#10416) - seals: refresh the three renamed port specs Co-Authored-By: Claude Opus 5.5 --- ...mmit_path_secret_aead_keying_mismatch.json | 28 ++ ...::src::commit_secret_export_collision.json | 28 ++ ...gs::CR-CHAT-03::src::commit_signature.json | 28 ++ ...R-CHAT-03::src::concurrent_add_remove.json | 28 ++ ...-CHAT-03::src::psk_external_injection.json | 28 ++ ...::ratchet_tree_blank_node_depth_guard.json | 28 ++ ...ding_block_alignment_uniformity_guard.json | 28 ++ ...ng_block_cipher_mode_uniformity_guard.json | 28 ++ ...ciphertext_length_class_balance_guard.json | 28 ++ ...dding_key_derivation_uniqueness_guard.json | 28 ++ ...padding_key_rotation_uniformity_guard.json | 28 ++ ...::padding_nonce_reuse_detection_guard.json | 28 ++ ...HAT-04::src::padding_oracle_chosen_ct.json | 28 ++ ...padding_payload_entropy_minimum_guard.json | 28 ++ ...ing_response_timing_fingerprint_guard.json | 28 ++ ...rc::padding_timing_side_channel_guard.json | 28 ++ ...:src::padding_timing_uniformity_guard.json | 28 ++ ...4::src::welcome_path_secret_unmasking.json | 28 ++ docs/reports/t27b_expectations.json | 18 + ...ommit_path_secret_aead_keying_mismatch.t27 | 372 ++++++++++++++ .../src/commit_secret_export_collision.t27 | 387 ++++++++++++++ .../rings/CR-CHAT-03/src/commit_signature.t27 | 246 +++++++++ .../CR-CHAT-03/src/concurrent_add_remove.t27 | 460 +++++++++++++++++ .../CR-CHAT-03/src/psk_external_injection.t27 | 385 ++++++++++++++ .../ratchet_tree_blank_node_depth_guard.t27 | 260 ++++++++++ ...dding_block_alignment_uniformity_guard.t27 | 348 +++++++++++++ ...ing_block_cipher_mode_uniformity_guard.t27 | 363 +++++++++++++ ..._ciphertext_length_class_balance_guard.t27 | 401 +++++++++++++++ ...adding_key_derivation_uniqueness_guard.t27 | 308 ++++++++++++ .../padding_key_rotation_uniformity_guard.t27 | 312 ++++++++++++ .../padding_nonce_reuse_detection_guard.t27 | 334 ++++++++++++ .../src/padding_oracle_chosen_ct.t27 | 372 ++++++++++++++ .../padding_payload_entropy_minimum_guard.t27 | 305 +++++++++++ ...ding_response_timing_fingerprint_guard.t27 | 476 ++++++++++++++++++ .../src/padding_timing_side_channel_guard.t27 | 350 +++++++++++++ .../src/padding_timing_uniformity_guard.t27 | 465 +++++++++++++++++ .../src/welcome_path_secret_unmasking.t27 | 346 +++++++++++++ tools/duplicate_bodies_baseline.txt | 10 + 38 files changed, 7022 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_path_secret_aead_keying_mismatch.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_secret_export_collision.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_signature.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::concurrent_add_remove.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::psk_external_injection.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_blank_node_depth_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_alignment_uniformity_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_cipher_mode_uniformity_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_class_balance_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_derivation_uniqueness_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_rotation_uniformity_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_nonce_reuse_detection_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_chosen_ct.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_side_channel_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_uniformity_guard.json create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::welcome_path_secret_unmasking.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_path_secret_aead_keying_mismatch.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_signature.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/concurrent_add_remove.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/psk_external_injection.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_blank_node_depth_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_alignment_uniformity_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_cipher_mode_uniformity_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_class_balance_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_uniqueness_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_rotation_uniformity_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_nonce_reuse_detection_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_chosen_ct.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_side_channel_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_uniformity_guard.t27 create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/welcome_path_secret_unmasking.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_path_secret_aead_keying_mismatch.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_path_secret_aead_keying_mismatch.json new file mode 100644 index 0000000000..2139cfa436 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_path_secret_aead_keying_mismatch.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:1d337eddf19bbf82fe1725768eb70607c4e7c857f8c533c9f626a99e9aede549", + "gen_hash_rust": "sha256:a837ee5955fec2e54a8343f519365f13cb5eb9eafbc2fa9e8eacdda8ea1c9d92", + "gen_hash_verilog": "sha256:beac8ea2200f0c79539a3a5492f2193767aac41849c158669745fad5af232eb2", + "gen_hash_zig": "sha256:245c8c81ae09f93672bfe0934f77100f169f3005b5e46cb7a3629f03ef302cf8", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_path_secret_aead_keying_mismatch", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T08:39:42Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:d98c635e1c59e51c2901de063b10f1f13d22cd2fd8c60134560aa621104e8cae", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_path_secret_aead_keying_mismatch.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_secret_export_collision.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_secret_export_collision.json new file mode 100644 index 0000000000..7ddfe11c0b --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_secret_export_collision.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:fc2470c4a69974fc2ad988d099340af362fdcd04201d87da6f5e8898b7beda0e", + "gen_hash_rust": "sha256:c35a375c6c951000a60b09e1ad975187a41b7ae19acb5f7f2725d80f30a4b6eb", + "gen_hash_verilog": "sha256:38e959ca974dc4527d13d160f02fdb50c37d648a02a714a2340a22bb5935aa85", + "gen_hash_zig": "sha256:1bdf14291f70202c098888c39834eb413d8ad6542c483ca0299ccff4ec28730d", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_secret_export_collision", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T08:50:35Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:a88a1c3145a9063879cf3633422969ca87517b5f6602adbcd39628b6d3293a4b", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 15, + "total": 15, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_signature.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_signature.json new file mode 100644 index 0000000000..30f55f73c4 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_signature.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:be20f438dccaebbbd587e10408b2b600bfd00744b9ad2d8ed9f9e772a3f9344a", + "gen_hash_rust": "sha256:0bedd81ef362b765f3b40bbffba01f3f562c96b2a07805cea66739962f0bb756", + "gen_hash_verilog": "sha256:5f9145d2a92a659392fdca31233636fb05a8038301f805779672f55f8b473a9e", + "gen_hash_zig": "sha256:f24be9ab393ce805a1250c07946da9b7ab8cebf82f48c58680fa4b43f9e9e7dd", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_signature", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T09:05:17Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:4313afdc6bf088d0209a9e70f3689465f0abf76ee0941fbddf9fa576b63183bd", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_signature.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::concurrent_add_remove.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::concurrent_add_remove.json new file mode 100644 index 0000000000..f5d41dfdec --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::concurrent_add_remove.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+7552b89d0", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:75c90cba95e392c982ca5c609c53491e9bcf92cb9b6fd8d26b85e2ebdccf806b", + "gen_hash_rust": "sha256:b214cb7b51d3af8d3bd7ec32c627c31e061eb3c302e217285396b5dba4ff9fc7", + "gen_hash_verilog": "sha256:20c0a5f41ff38517faaaa279419ab4d6d82ce47288f71ec638ea083e0f6bd8b9", + "gen_hash_zig": "sha256:542e1d66098f1a000719199e88eac891ace20a1a1746ecaaaff8c8452a3807f1", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-03::src::concurrent_add_remove", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T12:48:50Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:2d9ce772e7c9a669790174d96601dcc9e09633bde63e64d660cd5d8d9903b8f9", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/concurrent_add_remove.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 15, + "total": 15, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::psk_external_injection.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::psk_external_injection.json new file mode 100644 index 0000000000..cc9f72fb85 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::psk_external_injection.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+7552b89d0", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:bec76ba3a52cd416382baa4a1f3874a291f3564abed5d20fe8e915cc280522e5", + "gen_hash_rust": "sha256:712c71c57a55903b6ae9d9170fb6d3585b9eb2641a375c7eb1a39510a452018a", + "gen_hash_verilog": "sha256:efee1adfb10b29270e16a17198bd4a5931177c26444067c495cc9cfe32721002", + "gen_hash_zig": "sha256:71457ec3be814347bbe8779de3fa7a1e3b2e1977d0fce84a1b9bcf5394727938", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-03::src::psk_external_injection", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T12:48:53Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:afc620cacfae645a682fa28db28d452f0d1ab909040f9f3879a12df8f584e01c", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/psk_external_injection.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 14, + "total": 14, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_blank_node_depth_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_blank_node_depth_guard.json new file mode 100644 index 0000000000..77ba2cdd43 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_blank_node_depth_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+7552b89d0", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:88d493dcaf10b91a602d74e786998ccc6263c7874f1e6c216cf5b023a2b96a1d", + "gen_hash_rust": "sha256:6e3b49c8d5e1c2d8a5e60c79e0f736a88fc35161b2241a4b61e872e832880a0e", + "gen_hash_verilog": "sha256:a1f27d342d2cfc5498e11d0bb8c9d21b0fe0c885a0aebc38abaf70fb5ce09b34", + "gen_hash_zig": "sha256:c73ebb540393c3ee155061ea0c53206f947148efc16184b6ed1741b6030a8fad", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_blank_node_depth_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T12:48:53Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:b8b2366b590f7ff5e7d4318e685681ccb8990329a702dc8848251b0b74926750", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_blank_node_depth_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 11, + "total": 11, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_alignment_uniformity_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_alignment_uniformity_guard.json new file mode 100644 index 0000000000..1c678486f1 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_alignment_uniformity_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:b8dfe0b6ded7948d4337b94fd3ba671c0da5dfe5acf48c4b44e437f9b8c7a8fc", + "gen_hash_rust": "sha256:39f5b69f948f946494b33c7aac35bc13c9f680ad998d1b6a4df98a9c2074767b", + "gen_hash_verilog": "sha256:4c4b755a6e62e1105c0b469cde6f33a70a198d4adbbc9b65dc8582e06e546f43", + "gen_hash_zig": "sha256:f7b791190db5f2b250b863a1bcaf697c3b513596f33ccd0b1af42f6c1dc1dd31", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_alignment_uniformity_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T13:59:38Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:b854555b462de2345ed2911cbfbb98210e8a55d5d8371c3907bf8e1fe1551cdb", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_alignment_uniformity_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_cipher_mode_uniformity_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_cipher_mode_uniformity_guard.json new file mode 100644 index 0000000000..88d06d26a0 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_cipher_mode_uniformity_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:35d55166ef7e315b0ad32785b15b61a493edade6f6ec07003195d11c0205df9b", + "gen_hash_rust": "sha256:b7041cb856614753c97373f5c173ca9a1eb5d554ca55ffe861e54f7611530ebe", + "gen_hash_verilog": "sha256:2883e9f76eb4f382c6de4fcdc3d9e5b0e74477e551c7ee80ca1a237e54206c2e", + "gen_hash_zig": "sha256:4fb241944049c1638ac445beda1f9a9be1b7f6c2b1abb41c52ff431d64abf575", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_cipher_mode_uniformity_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T14:20:48Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:b50aa799bb83f04bf37219034571a1a6917ca9317a23deb30e6dd043185afb43", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_cipher_mode_uniformity_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_class_balance_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_class_balance_guard.json new file mode 100644 index 0000000000..bfe719eab0 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_class_balance_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:456fc9ccab722720a2d2620e17077b699b90fb11f593d073e69358366f138937", + "gen_hash_rust": "sha256:662abb07b13ed872013558f4320d5a3cc03c8b88596b51735586afe934263073", + "gen_hash_verilog": "sha256:20a7c9409cf5c11bd22fa2533bba3672e95626e132bb6e3455d33808290c1d5a", + "gen_hash_zig": "sha256:a92fd7b67a632fa0a86f45e1f0226acd981a24ff99f891d5f0cb819112cacfd9", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_class_balance_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T14:37:10Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:beb77569cb57018b5be023cc19fc1bc8dc9d8352f5124a06a9872dd14bda9509", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_class_balance_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_derivation_uniqueness_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_derivation_uniqueness_guard.json new file mode 100644 index 0000000000..8d9054adb8 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_derivation_uniqueness_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:b3bd141da6f7a19d6b9316be49a9ec947706e5b27cb28f2ea7a2c9466ff6002c", + "gen_hash_rust": "sha256:9c5a2a9c9d593484ca7563b270e6fbee8d7a2b41da6ffeb1c221ebaabbbd053d", + "gen_hash_verilog": "sha256:dd398487ef922e1afa5c06214ba91613f23fa1b150c43b93c75d954ede87f0e7", + "gen_hash_zig": "sha256:4f4c2a7c9f4c11db2d4a6231981972f6ed0a44955ba89d292e310f109f66dfa2", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_derivation_uniqueness_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T15:00:35Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:fa30cbd7d651a2b9284e2040ecacb09243aa0e3313bb27b135629d56591b6d6b", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_uniqueness_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_rotation_uniformity_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_rotation_uniformity_guard.json new file mode 100644 index 0000000000..9f0a565497 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_rotation_uniformity_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:540333d960db61399d83acce171e83405a3ca6e1db7e9f44e85fc35aede495ec", + "gen_hash_rust": "sha256:c0c9ce296474bcc4dc6f81fa3213a6ebdd9b277db1d3ab8c2ce23acb906fbb55", + "gen_hash_verilog": "sha256:5aaebf03f50fdd63dc1066f0ae8c13e6a579fb5d1f8b3bc2021e3d13df7252a4", + "gen_hash_zig": "sha256:94e3d788d059d6dcea3b78580424f1eb30875b08e92acf0bcda6cc74c9f72bfd", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_rotation_uniformity_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T15:05:47Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:027136f8d7c7e572d50b786715a7944473c4f3412a853a5ac7c6dfe3a76e153e", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_rotation_uniformity_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_nonce_reuse_detection_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_nonce_reuse_detection_guard.json new file mode 100644 index 0000000000..38d1c527af --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_nonce_reuse_detection_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:e09fb9e8609322c41c0b6a2eeef34f6b78e4658f2b15a0b2f4818095dc3c2d36", + "gen_hash_rust": "sha256:d6896ae0f3a1fc107b179595cf2e4100e829bcce92165fc0c7d26c941046df89", + "gen_hash_verilog": "sha256:25d44a0d44280b968fd2edea0fdd45b9ee206cc2be7a8163bfbb49cc5efd8606", + "gen_hash_zig": "sha256:80de8b3e760d686f40edf0a0c6fb46361965ca6e85d56ef1402192b4d9f5da98", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_nonce_reuse_detection_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T07:45:10Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:5fd4eef52999efffced9dd17cd07a74688ae32e35c6f00ec33dfdaf168366f02", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_nonce_reuse_detection_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 14, + "total": 14, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_chosen_ct.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_chosen_ct.json new file mode 100644 index 0000000000..3056fa389f --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_chosen_ct.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:6dc85b8551bf9c0b97403aaae6cbf1f88626be8e1e94998c8bb80420f1c51f74", + "gen_hash_rust": "sha256:838a68e81307d442de67057ff2fc7b76f1f358ee8dce7edc7e6e702dc332b0bf", + "gen_hash_verilog": "sha256:cba94b2bc52af009bd5757581993097170d15c5e744e9ce6e7380c385f6645a5", + "gen_hash_zig": "sha256:53ad54fb00fc65c2fe42316b9fcbcbccc990d9d7be6f2b3edd279df952fb5057", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_chosen_ct", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T12:19:58Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:73f207d94594f336b97997005f053fe48406f80bd1694058c3c55f48b9dc8e1a", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_chosen_ct.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard.json new file mode 100644 index 0000000000..e253577a07 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:87890c7ffd465adceacca323c30d5279583551cb8cb3cad31b1e6489d0bcb6bc", + "gen_hash_rust": "sha256:d1560e79d4ad438284274e2763c329d43d703f9ee12b87ad62fef4e934dfe808", + "gen_hash_verilog": "sha256:51e9cce2f4401bcce69d3502af6f968b590d65b5675300fca575af32b34d63d9", + "gen_hash_zig": "sha256:370872476e7a40802b33016fce49a99224465013f4d130e710b083d0b6ae7afd", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T12:41:31Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:64c82bb77b590befc7892383a699b459fba33b329727b600860eafca8e56a64c", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard.json new file mode 100644 index 0000000000..19e6a1dbe8 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:8f61a351374d4a00c669c61f176b0b74c51f64fd93a316df6c8b490589cba95a", + "gen_hash_rust": "sha256:b864dbb042f38b3a78c8fe6243a593d51d287a16c1c9d70801f5dc048f9fc337", + "gen_hash_verilog": "sha256:f9e5656aad11599fea0ea8cb0f851e887f71b1ae27e832f65219e549a8816f62", + "gen_hash_zig": "sha256:24b90070ec9b4b213eb788eced8bf1b124cf88fae569eaa941c5aa953272055c", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T13:10:11Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:36bd058647fc50b62f90f5d45704656b55e70fb1e3e742ce2edb99239decc07f", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_side_channel_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_side_channel_guard.json new file mode 100644 index 0000000000..9a9be43e07 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_side_channel_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:32e5362896fbe3c2246d9ce2a4f575fe634758fbe345de0c520c7b7552112dcd", + "gen_hash_rust": "sha256:508af79a4da1eff960657bbee77a30d13261d0f8c413c8f59cb753c77044a7e1", + "gen_hash_verilog": "sha256:30fa42e313f92aa2969a2e562457f4e720d39eb5d5ab97fcf1f29f8757d1341b", + "gen_hash_zig": "sha256:046a242254eee0dfe311e24ca912dfbf10a76216a8120a8273e8c66d5307cd6e", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_side_channel_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T13:19:00Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:5a09437bd8217906cac2d2b4141cda6592fb1a35ae994c502fa30c43111aa17b", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_side_channel_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_uniformity_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_uniformity_guard.json new file mode 100644 index 0000000000..549e892db9 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_uniformity_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:0ce087076500dcd0d7a57519711fd2afa74497d5826556159ade6b39626c62ac", + "gen_hash_rust": "sha256:025a79cdc1b24ddbf0529abda2881ae3b34d135995170bacf632a35a6c34742a", + "gen_hash_verilog": "sha256:1777f7dda70529b5248d74636b5031244670b1fcddbf28ed0c65289c1d00ee99", + "gen_hash_zig": "sha256:2816e889b972ed64b0ac648250dae19f400aa066e788d54ca3f8e95d94aa2fca", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_uniformity_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T13:48:14Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:971919a9e54207d4a90b486d3f8ebe0ae36783473f35e93e2b13069cc44ac6b7", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_uniformity_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::welcome_path_secret_unmasking.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::welcome_path_secret_unmasking.json new file mode 100644 index 0000000000..fae7999838 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::welcome_path_secret_unmasking.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:24dd121e66c38ee79de29c759f94d6563cb4dd8b9b2b587bd1cf31f4257618d0", + "gen_hash_rust": "sha256:ac038996d7cae42963b4e003b2e7352fa2136ef41545c1ca4d1e522b3486d8f4", + "gen_hash_verilog": "sha256:2bea91103b936a646f4321514ab654b17dcd1b3d12add3b886569f977bc3c87f", + "gen_hash_zig": "sha256:f22103d925fa50a97fcfcb27a1222443739a2a771da543c6f274cc97fbcd97fb", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::welcome_path_secret_unmasking", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T15:17:22Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:e7fd07ee662b8d6456304275982bdf6e6e93214726302d446db622bac1075793", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/welcome_path_secret_unmasking.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 09b79809ce..11ed345669 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1136,6 +1136,10 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-02/src/double_ratchet_sending_chain_rotation_guard.t27", "t27b": "blocked", "blocker": "type mismatch", "reason": "unimplemented"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-02/src/epoch_rollover_wraparound_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-02/src/skipped_message_key_gap_bound_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_path_secret_aead_keying_mismatch.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_signature.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/concurrent_add_remove.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/confirmation_tag_chain.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/external_commit.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/external_commit_resumption_psk_misbinding.t27", "t27b": "pass"}, @@ -1153,6 +1157,8 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/pcs_healing.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/proposal_ref_collision.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/proposal_validation.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/psk_external_injection.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_blank_node_depth_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_resolution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_update_integrity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/unmerged_leaves_bound_guard.t27", "t27b": "pass"}, @@ -1162,9 +1168,12 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mls_plaintext_framing_integrity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_alignment_boundary_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_alignment_uniformity_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_cipher_mode_uniformity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_pattern_uniformity_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_class_balance_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.t27", "t27b": "pass"}, @@ -1176,11 +1185,20 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_fill_byte_randomness_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_domain_separation_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_uniqueness_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_rotation_uniformity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_metadata_leak_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_nonce_reuse_detection_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_chosen_ct.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_side_channel_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_uniformity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/safety_number.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/welcome_path_secret_unmasking.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/wire_frame_integrity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/store_compaction_integrity_guard.t27", "t27b": "blocked", "blocker": "ExprArrayLiteral", "reason": "unimplemented"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_path_secret_aead_keying_mismatch.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_path_secret_aead_keying_mismatch.t27 new file mode 100644 index 0000000000..53cc762c6f --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_path_secret_aead_keying_mismatch.t27 @@ -0,0 +1,372 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_path_secret_aead_keying_mismatch; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-03/src/commit_path_secret_aead_keying_mismatch.rs +// (CR-CHAT-03 - Commit path-secret AEAD keying mismatch, +// Wave-36, L-CHAT-3-cpakm, R-CHAT-3, RFC 9420 7.7/8/12.4.3.2). +// +// A Commit that ships an UPDATE-PATH seals the new path +// secret once per leaf in each updated node's resolution, +// bound to the recipient's init key, to a nonce and to an +// AAD pinning the group, the epoch, the sender's leaf and +// the node. A mis-keyed slot - sealed to the wrong leaf, or +// with an AAD that lies about the node - poisons the whole +// epoch, and the receiver rejects on a single deny. The +// rules, in the original's fixed order: the canonical +// GroupID length (thirty-two), the epoch advancing by +// exactly one, the sender's leaf inside the tree, the +// receiver's resolution slot present, the ciphertext length +// canonical (forty-eight - the sealed secret plus the +// Poly1305 tag, rejected before any decap), the recipient +// init key equal to the local one, and the AAD context equal +// to the canonical encoding of group, epoch, sender leaf +// and node - with the slot's own node index answering the +// same rule. +// +// The original returns Result<(), PathSecretAeadKeyingError>. +// This port carries the verdict as a named struct (owner +// decision D5): Ok first, then the seven variants in the +// original's declaration order - NonCanonicalGroupIdLength, +// EpochMismatch, SenderLeafOutOfRange, +// ResolutionSlotOutOfRange, RecipientInitKeyMismatch, +// AadContextMismatch, NonCanonicalCiphertextLength. Every +// original variant carries no payload, so the code is the +// whole verdict. +// +// The original's commit and view structs become flat +// parameters (slices as parameters); the slot stays a +// record. The Vec fields become fixed capacities plus +// lengths where a rule reads the length: the ciphertext +// gains a sixty-four capacity with its length (the original's +// short and long fixtures live there), the GroupID arrives +// as a plain byte slice whose length rule one reads, and the +// init key and AAD context stay at their canonical +// capacities - every original fixture carries them +// canonical, only the content ever disagrees. The canonical +// AAD encoding is answered by a positional matcher - the +// first thirty-two bytes the GroupID, then the big-endian +// epoch, sender leaf and node index read from a table of +// powers of two hundred fifty-six (t27 has no shifts by a +// variable amount; the table walks are exact). The original's +// saturating epoch advance differs from the plain successor +// only at the top of u64, where both answer the same +// maximum - the port spells the edge explicitly. + +// Canonical MLS GroupID length (32 bytes). +pub const CPAKM_GROUP_ID_LEN: usize = 32; + +// Canonical HPKE init_key length (32 bytes). +pub const CPAKM_INIT_KEY_LEN: usize = 32; + +// Canonical AAD-context length (32 + 8 + 8 + 8 = 56 bytes). +pub const CPAKM_AAD_CONTEXT_LEN: usize = 56; + +// Canonical path-secret ciphertext length (32 + 16 = 48 bytes). +pub const CPAKM_PATH_SECRET_CIPHERTEXT_LEN: usize = 48; + +// The ciphertext record capacity: the original's longest +// fixture is sixty-four bytes. +pub const CPAKM_CIPHERTEXT_CAP: usize = 64; + +// The top of u64 - the saturating epoch edge. +pub const CPAKM_U64_TOP: u64 = 18446744073709551615; + +// The big-endian byte weights of a u64, as a table - byte k +// of the encoding is (x / P256[k]) mod 256. +pub const P256: [8]u64 = [8]u64{1, 256, 65536, 16777216, 4294967296, 1099511627776, 281474976710656, 72057594037927936,} + +// One HPKE encryption inside a Commit's UPDATE-PATH, +// addressed to a single leaf in a node's resolution. +pub struct UpdatePathSlot { + node_index: u64, + recipient_init_key: [32]u8, + aad_context: [56]u64, + ciphertext: [64]u8, + ciphertext_len: usize, +} + +// Verdict codes: Ok(()) and PathSecretAeadKeyingError's variants, in declaration order. +pub enum CpakmCode { + Ok, + NonCanonicalGroupIdLength, + EpochMismatch, + SenderLeafOutOfRange, + ResolutionSlotOutOfRange, + RecipientInitKeyMismatch, + AadContextMismatch, + NonCanonicalCiphertextLength, +} + +// The verdict of the path-secret validation - the original's +// variants carry no payload, so the code alone carries it. +pub struct CpakmVerdict { + code: CpakmCode, +} + +// Whole-key equality over the canonical init-key capacity - +// the original's Vec equality on keys that every fixture +// carries canonical. +pub fn init_keys_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether the AAD context equals the canonical encoding +// group_id then epoch then sender leaf then node index, all +// big-endian - the original's Vec extend walk answered +// positionally, the GroupID already proven canonical by rule +// one. +pub fn aad_context_matches(aad: [56]u64, group_id: []const u8, epoch: u64, sender_leaf: u64, node_index: u64) -> bool { + for i in 0..32 { + if (aad[i] != @as(u64, group_id[i])) { + return false; + } + } + for k in 0..8 { + if (aad[32 + k] != (epoch / P256[7 - k]) % 256) { + return false; + } + if (aad[40 + k] != (sender_leaf / P256[7 - k]) % 256) { + return false; + } + if (aad[48 + k] != (node_index / P256[7 - k]) % 256) { + return false; + } + } + return true; +} + +// [VERIFIED] Validate a single Commit's UPDATE-PATH slot +// targeted at the local receiver. Returns Ok only when every +// rule holds, checked in the original's fixed order: the +// GroupID length, the epoch advance, the sender's leaf, the +// slot's presence, the ciphertext length before any crypto +// binding, the recipient init key, then the AAD context with +// the slot's node index. +pub fn validate_commit_path_secret(group_id: []const u8, epoch: u64, sender_leaf: u64, slots: []const UpdatePathSlot, local_epoch: u64, tree_size: u64, local_init_key: [32]u8, local_resolution_index: usize, local_node_index: u64) -> CpakmVerdict { + if (group_id.len != CPAKM_GROUP_ID_LEN) { + return CpakmVerdict { code: CpakmCode.NonCanonicalGroupIdLength, }; + } + var expected_epoch: u64 = CPAKM_U64_TOP; + if (local_epoch < CPAKM_U64_TOP) { + expected_epoch = local_epoch + 1; + } + if (epoch != expected_epoch) { + return CpakmVerdict { code: CpakmCode.EpochMismatch, }; + } + if (sender_leaf >= tree_size) { + return CpakmVerdict { code: CpakmCode.SenderLeafOutOfRange, }; + } + if (local_resolution_index >= slots.len) { + return CpakmVerdict { code: CpakmCode.ResolutionSlotOutOfRange, }; + } + if (slots[local_resolution_index].ciphertext_len != CPAKM_PATH_SECRET_CIPHERTEXT_LEN) { + return CpakmVerdict { code: CpakmCode.NonCanonicalCiphertextLength, }; + } + if (!init_keys_equal(slots[local_resolution_index].recipient_init_key, local_init_key)) { + return CpakmVerdict { code: CpakmCode.RecipientInitKeyMismatch, }; + } + if (!aad_context_matches(slots[local_resolution_index].aad_context, group_id, epoch, sender_leaf, local_node_index)) { + return CpakmVerdict { code: CpakmCode.AadContextMismatch, }; + } + if (slots[local_resolution_index].node_index != local_node_index) { + return CpakmVerdict { code: CpakmCode.AadContextMismatch, }; + } + return CpakmVerdict { code: CpakmCode.Ok, }; +} + +// The fixture bytes: the GroupID of eleven-hex-eleven, the +// local init key of twenty-two-hex, the stranger's key of +// thirty-three-hex, the ciphertext of forty-four-hex over +// its full capacity, and the canonical AAD contexts of the +// fixtures. +pub const GOOD_GROUP_ID: [32]u8 = [32]u8{17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,} +pub const LOCAL_INIT_KEY: [32]u8 = [32]u8{34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,} +pub const OTHER_INIT_KEY: [32]u8 = [32]u8{51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,} +pub const GOOD_CIPHERTEXT: [64]u8 = [64]u8{68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,} +pub const OK_AAD: [56]u64 = [56]u8{17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 7,} + +// **CPAKM-01** - the short GroupID rejected: sixteen bytes of +// a canonical thirty-two. +test cpakm_01_short_group_id_rejected { + var short_gid: [16]u8; + for j in 0..16 { + short_gid[j] = 17; + } + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&short_gid, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.NonCanonicalGroupIdLength); +} + +// **CPAKM-02** - the epoch skip rejected: forty-four of a +// required forty-two over the local forty-one. +test cpakm_02_epoch_skip_rejected { + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 44, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.EpochMismatch); +} + +// **CPAKM-03** - the sender's leaf out of range rejected: +// sixteen of a tree of sixteen, the AAD adjusted to the bad +// sender exactly as the original's fixture does. +test cpakm_03_sender_leaf_out_of_range_rejected { + var sender16_aad: [56]u64 = OK_AAD; + for k in 0..8 { + sender16_aad[40 + k] = (16 / P256[7 - k]) % 256; + } + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: sender16_aad, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 16, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.SenderLeafOutOfRange); +} + +// **CPAKM-04** - the receiver's resolution slot out of range +// rejected: index one of a single slot. +test cpakm_04_resolution_slot_out_of_range_rejected { + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 1, 7); + assert(v.code == CpakmCode.ResolutionSlotOutOfRange); +} + +// **CPAKM-05** - the recipient init key mismatch rejected: +// the stranger's key in the slot. +test cpakm_05_recipient_init_key_mismatch_rejected { + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: OTHER_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.RecipientInitKeyMismatch); +} + +// **CPAKM-06** - the AAD context with the wrong epoch +// rejected: nine hundred ninety-nine of a canonical +// forty-two. +test cpakm_06_aad_context_epoch_mismatch_rejected { + var epoch999_aad: [56]u64 = OK_AAD; + for k in 0..8 { + epoch999_aad[32 + k] = (999 / P256[7 - k]) % 256; + } + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: epoch999_aad, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.AadContextMismatch); +} + +// **CPAKM-07** - the AAD context disagreeing on the node +// index rejected: the slot claims node nine of a local seven, +// the AAD encoding nine with it exactly as the original's +// fixture does. +test cpakm_07_aad_context_node_index_mismatch_rejected { + var node9_aad: [56]u64 = OK_AAD; + for k in 0..8 { + node9_aad[48 + k] = (9 / P256[7 - k]) % 256; + } + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 9, recipient_init_key: LOCAL_INIT_KEY, aad_context: node9_aad, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.AadContextMismatch); +} + +// **CPAKM-08** - the short ciphertext rejected: thirty-two +// bytes, the sealed secret without its tag. +test cpakm_08_short_ciphertext_rejected { + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 32, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.NonCanonicalCiphertextLength); +} + +// **CPAKM-09** - the long ciphertext rejected: sixty-four +// bytes of a canonical forty-eight. +test cpakm_09_long_ciphertext_rejected { + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 64, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.NonCanonicalCiphertextLength); +} + +// **CPAKM-10** - the canonical update-path slot accepted. +test cpakm_10_canonical_commit_accepted { + const slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v.code == CpakmCode.Ok); +} + +// The helpers have direct tests (coverage: Untested 0). +test init_keys_equal_answers { + assert(init_keys_equal(LOCAL_INIT_KEY, LOCAL_INIT_KEY)); + assert(!init_keys_equal(LOCAL_INIT_KEY, OTHER_INIT_KEY)); +} + +test aad_context_matches_answers { + assert(aad_context_matches(OK_AAD, &GOOD_GROUP_ID, 42, 3, 7)); + assert(!aad_context_matches(OK_AAD, &GOOD_GROUP_ID, 999, 3, 7)); + assert(!aad_context_matches(OK_AAD, &GOOD_GROUP_ID, 42, 3, 9)); +} + +// Rule order is the original's fixed ladder, each probe +// stacking the violation of the earlier rule on the later +// one: a short GroupID over an epoch skip, the epoch skip +// over a phantom sender, the phantom sender over a missing +// slot, the missing slot over a short ciphertext, the short +// ciphertext over a stranger's key (the pre-decap order), +// the stranger's key over a lying AAD - and the slot's node +// index alone answering the AAD rule. +test rule_order_matches_original { + var short_gid: [16]u8; + for j in 0..16 { + short_gid[j] = 17; + } + const ok_slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v1 = validate_commit_path_secret(&short_gid, 44, 16, &ok_slots, 41, 16, LOCAL_INIT_KEY, 1, 7); + assert(v1.code == CpakmCode.NonCanonicalGroupIdLength); + + const v2 = validate_commit_path_secret(&GOOD_GROUP_ID, 44, 16, &ok_slots, 41, 16, LOCAL_INIT_KEY, 1, 7); + assert(v2.code == CpakmCode.EpochMismatch); + + const v3 = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 16, &ok_slots, 41, 16, LOCAL_INIT_KEY, 1, 7); + assert(v3.code == CpakmCode.SenderLeafOutOfRange); + + const v4 = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &ok_slots, 41, 16, LOCAL_INIT_KEY, 1, 7); + assert(v4.code == CpakmCode.ResolutionSlotOutOfRange); + + const bad_ct_slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: OTHER_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 32, }, + }; + const v5 = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &bad_ct_slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v5.code == CpakmCode.NonCanonicalCiphertextLength); + + var lying_aad: [56]u64 = OK_AAD; + for k in 0..8 { + lying_aad[32 + k] = (999 / P256[7 - k]) % 256; + } + const wrong_key_slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 7, recipient_init_key: OTHER_INIT_KEY, aad_context: lying_aad, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v6 = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &wrong_key_slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v6.code == CpakmCode.RecipientInitKeyMismatch); + + const node9_slots: [1]UpdatePathSlot = [1]UpdatePathSlot{ + UpdatePathSlot { node_index: 9, recipient_init_key: LOCAL_INIT_KEY, aad_context: OK_AAD, ciphertext: GOOD_CIPHERTEXT, ciphertext_len: 48, }, + }; + const v7 = validate_commit_path_secret(&GOOD_GROUP_ID, 42, 3, &node9_slots, 41, 16, LOCAL_INIT_KEY, 0, 7); + assert(v7.code == CpakmCode.AadContextMismatch); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.t27 new file mode 100644 index 0000000000..12f5cda2b1 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.t27 @@ -0,0 +1,387 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_secret_export_collision; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.rs +// (CR-CHAT-03 - Commit-secret export collision, Wave-33, +// L-CHAT-3-csec, R-CHAT-11, RFC 9420 8.4/9). +// +// After each Commit the deriving member exports a fresh +// commit secret from the KDF tree, bound to the group, the +// epoch and the commit transcript hash. A hostile member +// splices across groups or epochs, replays a published +// secret, binds to a phantom transcript or submits the +// zero sentinel; a single deny wins. The rules, in the +// original's fixed order: the canonical secret length +// (thirty-two), the non-empty transcript hash, the +// transcript present in the local ledger, the group equal to +// the expected one, the epoch equal to the current one, the +// (transcript, secret) pair fresh against every prior +// export, and the secret not the all-zero sentinel. +// +// The original returns Result<(), CommitSecretError>. This +// port carries the verdict as a named struct (owner decision +// D5): Ok first, then the seven variants in the original's +// declaration order - NonCanonicalCommitSecretLength, +// EmptyTranscriptHash, UnknownTranscriptHash, +// CrossGroupCommitSecret, StaleEpochCommitSecret, +// CommitSecretReplay, ZeroCommitSecret. Every original +// variant carries no payload, so the code is the whole +// verdict. +// +// The original's export and view structs become flat +// parameters, the byte vectors arriving as plain slices. +// The vector of known transcript hashes becomes a slice of +// fixed-capacity records (sixty-four bytes, the original's +// generous transcript upper bound), and the vector of +// exported pairs becomes a slice of records holding the +// transcript and the secret with their lengths - the +// original's Vec contains and tuple contains become scans +// (conventions section 4), each comparison a length gate +// then a byte walk. + +// Canonical commit_secret length (32 bytes). +pub const COMMIT_SECRET_LEN: usize = 32; + +// Maximum transcript_hash byte length accepted. +pub const COMMIT_TRANSCRIPT_HASH_MAX_LEN: usize = 64; + +// A transcript hash the local commit ledger has observed. +pub struct TranscriptRef { + bytes: [64]u8, + hash_len: usize, +} + +// A (transcript_hash, commit_secret) pair already consumed +// by a prior commit export. +pub struct ExportedPair { + hash: [64]u8, + hash_len: usize, + secret: [64]u8, + secret_len: usize, +} + +// Verdict codes: Ok(()) and CommitSecretError's variants, in declaration order. +pub enum CsecCode { + Ok, + NonCanonicalCommitSecretLength, + EmptyTranscriptHash, + UnknownTranscriptHash, + CrossGroupCommitSecret, + StaleEpochCommitSecret, + CommitSecretReplay, + ZeroCommitSecret, +} + +// The verdict of the commit-secret export validation - the +// original's variants carry no payload, so the code alone +// carries it. +pub struct CsecVerdict { + code: CsecCode, +} + +// Content equality over two byte slices - the original's +// Vec equality on the group identifiers. +pub fn group_ids_equal(a: []const u8, b: []const u8) -> bool { + if (a.len != b.len) { + return false; + } + for i in 0..a.len { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether the ledger knows this transcript hash - the +// original's Vec contains over the known hashes, answered by +// a scan over the fixed-capacity records. +pub fn transcript_known(known: []const TranscriptRef, hash: []const u8) -> bool { + for i in 0..known.len { + if (known[i].hash_len == hash.len) { + var same: bool = true; + for j in 0..hash.len { + if (known[i].bytes[j] != hash[j]) { + same = false; + } + } + if (same) { + return true; + } + } + } + return false; +} + +// Whether this (transcript, secret) pair was already +// exported - the original's tuple contains, answered by a +// scan over the pair records. +pub fn pair_exported(pairs: []const ExportedPair, hash: []const u8, secret: []const u8) -> bool { + for i in 0..pairs.len { + if (pairs[i].hash_len == hash.len) { + if (pairs[i].secret_len == secret.len) { + var same: bool = true; + for j in 0..hash.len { + if (pairs[i].hash[j] != hash[j]) { + same = false; + } + } + for j in 0..secret.len { + if (pairs[i].secret[j] != secret[j]) { + same = false; + } + } + if (same) { + return true; + } + } + } + } + return false; +} + +// Whether every byte is zero - the original's all-zero +// sentinel probe. +pub fn all_zero_bytes(bytes: []const u8) -> bool { + for i in 0..bytes.len { + if (bytes[i] != 0) { + return false; + } + } + return true; +} + +// [VERIFIED] Validate an exported commit secret. Returns Ok +// only when every rule holds, checked in the original's +// fixed order: the canonical length, the non-empty +// transcript, the known transcript, the expected group, the +// current epoch, the fresh pair and the non-zero secret. +pub fn validate_commit_secret_export(commit_secret: []const u8, transcript_hash: []const u8, group_id: []const u8, commit_epoch: u64, expected_group_id: []const u8, current_epoch: u64, known_hashes: []const TranscriptRef, exported_pairs: []const ExportedPair) -> CsecVerdict { + if (commit_secret.len != COMMIT_SECRET_LEN) { + return CsecVerdict { code: CsecCode.NonCanonicalCommitSecretLength, }; + } + if (transcript_hash.len == 0) { + return CsecVerdict { code: CsecCode.EmptyTranscriptHash, }; + } + if (!transcript_known(known_hashes, transcript_hash)) { + return CsecVerdict { code: CsecCode.UnknownTranscriptHash, }; + } + if (!group_ids_equal(group_id, expected_group_id)) { + return CsecVerdict { code: CsecCode.CrossGroupCommitSecret, }; + } + if (commit_epoch != current_epoch) { + return CsecVerdict { code: CsecCode.StaleEpochCommitSecret, }; + } + if (pair_exported(exported_pairs, transcript_hash, commit_secret)) { + return CsecVerdict { code: CsecCode.CommitSecretReplay, }; + } + if (all_zero_bytes(commit_secret)) { + return CsecVerdict { code: CsecCode.ZeroCommitSecret, }; + } + return CsecVerdict { code: CsecCode.Ok, }; +} + +// The fixture bytes: the canonical secret of forty-four-hex, +// the zero sentinel, the fresh distinct secret of +// double-C-hex, the transcripts and the group identifiers of +// the original's fixtures. +pub const CANONICAL_SECRET: [32]u8 = [32]u8{68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,} +pub const ZERO_SECRET: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} +pub const FRESH_SECRET: [32]u8 = [32]u8{204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204,} +pub const PAIR_CANONICAL: [64]u8 = [64]u8{68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} +pub const PAIR_FRESH: [64]u8 = [64]u8{204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} +pub const PAIR_ZERO: [64]u8 = [64]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} +pub const TRANS_A: [12]u8 = [12]u8{116, 114, 97, 110, 115, 99, 114, 105, 112, 116, 45, 65,} +pub const TRANS_B: [12]u8 = [12]u8{116, 114, 97, 110, 115, 99, 114, 105, 112, 116, 45, 66,} +pub const PHANTOM: [18]u8 = [18]u8{112, 104, 97, 110, 116, 111, 109, 45, 116, 114, 97, 110, 115, 99, 114, 105, 112, 116,} +pub const GOOD_GROUP: [16]u8 = [16]u8{116, 114, 105, 110, 105, 116, 121, 45, 103, 114, 111, 117, 112, 45, 51, 51,} +pub const OTHER_GROUP: [19]u8 = [19]u8{116, 114, 105, 110, 105, 116, 121, 45, 103, 114, 111, 117, 112, 45, 79, 84, 72, 69, 82,} + +// The canonical view's known transcripts. +pub const KNOWN_HASHES: [2]TranscriptRef = [2]TranscriptRef{ + TranscriptRef { bytes: [64]u8{116, 114, 97, 110, 115, 99, 114, 105, 112, 116, 45, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, hash_len: 12, }, + TranscriptRef { bytes: [64]u8{116, 114, 97, 110, 115, 99, 114, 105, 112, 116, 45, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, hash_len: 12, }, +}; + +// The 64-capacity forms of the pair-record secrets: the +// record capacity is sixty-four, the canonical length rides +// the length field. + +// **CSEC-01** - the sixteen-byte commit secret rejected. +test csec_01_short_commit_secret_rejected { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + var short_secret: [16]u8; + for j in 0..16 { + short_secret[j] = 68; + } + const v = validate_commit_secret_export(&short_secret, &TRANS_A, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.NonCanonicalCommitSecretLength); +} + +// **CSEC-02** - the sixty-four-byte commit secret rejected. +test csec_02_over_long_commit_secret_rejected { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + var long_secret: [64]u8; + for j in 0..64 { + long_secret[j] = 68; + } + const v = validate_commit_secret_export(&long_secret, &TRANS_A, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.NonCanonicalCommitSecretLength); +} + +// **CSEC-03** - the empty transcript hash rejected. +test csec_03_empty_transcript_hash_rejected { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + const empty_hash: [0]u8 = [0]u8{}; + const v = validate_commit_secret_export(&CANONICAL_SECRET, &empty_hash, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.EmptyTranscriptHash); +} + +// **CSEC-04** - the phantom transcript hash rejected: not in +// the ledger. +test csec_04_unknown_transcript_hash_rejected { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + const v = validate_commit_secret_export(&CANONICAL_SECRET, &PHANTOM, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.UnknownTranscriptHash); +} + +// **CSEC-05** - the cross-group commit secret rejected. +test csec_05_cross_group_commit_secret_rejected { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + const v = validate_commit_secret_export(&CANONICAL_SECRET, &TRANS_A, &OTHER_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.CrossGroupCommitSecret); +} + +// **CSEC-06** - the stale-epoch commit secret rejected: +// eleven of a current twelve. +test csec_06_stale_epoch_commit_secret_rejected { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + const v = validate_commit_secret_export(&CANONICAL_SECRET, &TRANS_A, &GOOD_GROUP, 11, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.StaleEpochCommitSecret); +} + +// **CSEC-07** - the replayed (transcript, secret) pair +// rejected: this very pair was exported before. +test csec_07_replayed_commit_secret_rejected { + var hash_buf: [64]u8; + for j in 0..64 { + hash_buf[j] = 0; + } + for j in 0..12 { + hash_buf[j] = TRANS_A[j]; + } + const pairs: [1]ExportedPair = [1]ExportedPair{ + ExportedPair { hash: hash_buf, hash_len: 12, secret: PAIR_CANONICAL, secret_len: 32, }, + }; + const v = validate_commit_secret_export(&CANONICAL_SECRET, &TRANS_A, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &pairs); + assert(v.code == CsecCode.CommitSecretReplay); +} + +// **CSEC-08** - the all-zero commit secret rejected. +test csec_08_zero_commit_secret_rejected { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + const v = validate_commit_secret_export(&ZERO_SECRET, &TRANS_A, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.ZeroCommitSecret); +} + +// **CSEC-09** - the canonical commit secret accepted. +test csec_09_canonical_commit_secret_accepted { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + const v = validate_commit_secret_export(&CANONICAL_SECRET, &TRANS_A, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v.code == CsecCode.Ok); +} + +// **CSEC-10** - the same transcript with a fresh distinct +// secret accepted: the replay rule reads the pair, never the +// hash alone. +test csec_10_distinct_fresh_commit_secret_accepted { + var hash_buf: [64]u8; + for j in 0..64 { + hash_buf[j] = 0; + } + for j in 0..12 { + hash_buf[j] = TRANS_A[j]; + } + const pairs: [1]ExportedPair = [1]ExportedPair{ + ExportedPair { hash: hash_buf, hash_len: 12, secret: PAIR_FRESH, secret_len: 32, }, + }; + const v = validate_commit_secret_export(&CANONICAL_SECRET, &TRANS_A, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &pairs); + assert(v.code == CsecCode.Ok); +} + +// The helpers have direct tests (coverage: Untested 0). +test group_ids_equal_answers { + assert(group_ids_equal(&GOOD_GROUP, &GOOD_GROUP)); + assert(!group_ids_equal(&GOOD_GROUP, &OTHER_GROUP)); +} + +test transcript_known_answers { + assert(transcript_known(&KNOWN_HASHES, &TRANS_A)); + assert(transcript_known(&KNOWN_HASHES, &TRANS_B)); + assert(!transcript_known(&KNOWN_HASHES, &PHANTOM)); +} + +test pair_exported_answers { + var hash_buf: [64]u8; + for j in 0..64 { + hash_buf[j] = 0; + } + for j in 0..12 { + hash_buf[j] = TRANS_A[j]; + } + const pairs: [2]ExportedPair = [2]ExportedPair{ + ExportedPair { hash: hash_buf, hash_len: 12, secret: PAIR_CANONICAL, secret_len: 32, }, + ExportedPair { hash: hash_buf, hash_len: 12, secret: PAIR_FRESH, secret_len: 32, }, + }; + assert(pair_exported(&pairs, &TRANS_A, &CANONICAL_SECRET)); + assert(!pair_exported(&pairs, &TRANS_A, &ZERO_SECRET)); + assert(!pair_exported(&pairs, &PHANTOM, &CANONICAL_SECRET)); +} + +test all_zero_bytes_answers { + assert(all_zero_bytes(&ZERO_SECRET)); + assert(!all_zero_bytes(&CANONICAL_SECRET)); +} + +// Rule order is the original's fixed ladder, each probe +// stacking the earlier rule's violation on the later one: a +// short secret over an empty transcript, the empty +// transcript over the phantom, the phantom over the wrong +// group, the wrong group over the stale epoch, the stale +// epoch over the replayed pair, and the replayed pair over +// the zero sentinel. +test rule_order_matches_original { + const no_pairs: [0]ExportedPair = [0]ExportedPair{}; + var short_secret: [16]u8; + for j in 0..16 { + short_secret[j] = 68; + } + const empty_hash: [0]u8 = [0]u8{}; + + const v1 = validate_commit_secret_export(&short_secret, &empty_hash, &OTHER_GROUP, 11, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v1.code == CsecCode.NonCanonicalCommitSecretLength); + + const v2 = validate_commit_secret_export(&CANONICAL_SECRET, &empty_hash, &OTHER_GROUP, 11, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v2.code == CsecCode.EmptyTranscriptHash); + + const v3 = validate_commit_secret_export(&CANONICAL_SECRET, &PHANTOM, &OTHER_GROUP, 11, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v3.code == CsecCode.UnknownTranscriptHash); + + const v4 = validate_commit_secret_export(&CANONICAL_SECRET, &TRANS_A, &OTHER_GROUP, 11, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v4.code == CsecCode.CrossGroupCommitSecret); + + const v5 = validate_commit_secret_export(&CANONICAL_SECRET, &TRANS_A, &GOOD_GROUP, 11, &GOOD_GROUP, 12, &KNOWN_HASHES, &no_pairs); + assert(v5.code == CsecCode.StaleEpochCommitSecret); + + var hash_buf: [64]u8; + for j in 0..64 { + hash_buf[j] = 0; + } + for j in 0..12 { + hash_buf[j] = TRANS_A[j]; + } + const zero_pairs: [1]ExportedPair = [1]ExportedPair{ + ExportedPair { hash: hash_buf, hash_len: 12, secret: PAIR_ZERO, secret_len: 32, }, + }; + const v6 = validate_commit_secret_export(&ZERO_SECRET, &TRANS_A, &GOOD_GROUP, 12, &GOOD_GROUP, 12, &KNOWN_HASHES, &zero_pairs); + assert(v6.code == CsecCode.CommitSecretReplay); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_signature.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_signature.t27 new file mode 100644 index 0000000000..fffdf7dea0 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_signature.t27 @@ -0,0 +1,246 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_signature; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-03/src/commit_signature.rs +// (CR-CHAT-03 - Commit signature forgery defense, Wave-24, +// Lane A, L-CHAT-3-csig, R-CHAT-3, RFC 9420 6.1/12). +// +// Every MLS Commit is signed by its committer leaf key. The +// forgery gate pins the binding invariants before the caller +// delegates to the real signature check: a zero-length or +// all-zero signature blob is a lazy-verifier trap, a stale +// pre-rotation key must not verify, a transcript from +// another group, another epoch or another commit body is a +// splice, and a removed leaf must not sign at all. The +// rules, in the original's fixed order: the blob non-empty +// and non-zero, the group agreement, the epoch agreement, +// the committer a current member, the signer key the stored +// leaf-public, and the ops hash the locally computed one. +// +// The original returns Result<(), CommitSigError>. This +// port carries the verdict as a named struct (owner decision +// D5): Ok first, then the six variants in the original's +// declaration order - EmptySignature, StaleSignerKey, +// GroupIdSplice, EpochMismatch, NonMemberSigner, +// TranscriptMismatch - with every payload field: the +// EpochMismatch carries the receiver's current epoch and +// the transcript's claimed one. +// +// The original's transcript and view structs become flat +// parameters, the signature blob arriving as a plain slice. +// The view's Option<[u8; 32]> committer key becomes a +// member flag plus the stored key (the key unread when the +// flag answers non-member) - the sentinel convention. The +// transcript's sender leaf rides along unread by every rule, +// exactly as in the original. The original's tenth test is +// a tautology green summary (a count asserted against +// itself); this port replaces it with a live gate call, the +// WPS-10 precedent, so no test passes vacuously. + +// Verdict codes: Ok(()) and CommitSigError's variants, in declaration order. +pub enum CsfCode { + Ok, + EmptySignature, + StaleSignerKey, + GroupIdSplice, + EpochMismatch, + NonMemberSigner, + TranscriptMismatch, +} + +// The verdict of the commit-signature gate, with the one +// payload pair: the current and claimed epochs of the epoch +// rule. +pub struct CsfVerdict { + code: CsfCode, + current: u64, + claimed: u64, +} + +// Whole-form equality over the thirty-two-byte keys, group +// identifiers and ops hashes - the original's array equality. +pub fn bytes32_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether every byte of the blob is zero - the original's +// all-zero probe of the lazy-verifier trap. +pub fn all_zero_bytes(blob: []const u8) -> bool { + for i in 0..blob.len { + if (blob[i] != 0) { + return false; + } + } + return true; +} + +// [VERIFIED] Verify the binding layer of a signed commit. +// Returns Ok only when every rule holds, checked in the +// original's fixed order: the blob non-empty and non-zero, +// the group, the epoch, the membership, the signer key and +// the ops hash. +pub fn verify_commit_signature(t_group_id: [32]u8, from_epoch: u64, sender_leaf: u32, t_ops_hash: [32]u8, sig_pubkey: [32]u8, sig_blob: []const u8, view_group_id: [32]u8, current_epoch: u64, committer_is_member: bool, committer_leaf_key: [32]u8, local_ops_hash: [32]u8) -> CsfVerdict { + if (sig_blob.len == 0) { + return CsfVerdict { code: CsfCode.EmptySignature, current: 0, claimed: 0, }; + } + if (all_zero_bytes(sig_blob)) { + return CsfVerdict { code: CsfCode.EmptySignature, current: 0, claimed: 0, }; + } + if (!bytes32_equal(t_group_id, view_group_id)) { + return CsfVerdict { code: CsfCode.GroupIdSplice, current: 0, claimed: 0, }; + } + if (from_epoch != current_epoch) { + return CsfVerdict { code: CsfCode.EpochMismatch, current: current_epoch, claimed: from_epoch, }; + } + if (!committer_is_member) { + return CsfVerdict { code: CsfCode.NonMemberSigner, current: 0, claimed: 0, }; + } + if (!bytes32_equal(sig_pubkey, committer_leaf_key)) { + return CsfVerdict { code: CsfCode.StaleSignerKey, current: 0, claimed: 0, }; + } + if (!bytes32_equal(t_ops_hash, local_ops_hash)) { + return CsfVerdict { code: CsfCode.TranscriptMismatch, current: 0, claimed: 0, }; + } + return CsfVerdict { code: CsfCode.Ok, current: 0, claimed: 0, }; +} + +// The fixture bytes: the group of eleven-hex, the ops hash +// of twenty-two-hex, the leaf key of thirty-three-hex, the +// splice probes and the sixty-four-byte signature blob of +// double-A-hex. +pub const GOOD_GID: [32]u8 = [32]u8{17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,} +pub const GOOD_OPS: [32]u8 = [32]u8{34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,} +pub const LEAF_KEY: [32]u8 = [32]u8{51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,} +pub const BAD_GID: [32]u8 = [32]u8{204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204,} +pub const STALE_KEY: [32]u8 = [32]u8{238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238,} +pub const BAD_OPS: [32]u8 = [32]u8{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,} +pub const GOOD_BLOB: [64]u8 = [64]u8{171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,} +pub const ZERO_BLOB: [64]u8 = [64]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} + +// The canonical gate call of the fixtures: the good commit +// against the good view. +fn canonical_call() -> CsfVerdict { + const v = verify_commit_signature(GOOD_GID, 4, 7, GOOD_OPS, LEAF_KEY, &GOOD_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + return v; +} + +// **CSF-01** - the happy path accepted: every field matched. +test csf_01_happy_path_accepted { + const v = canonical_call(); + assert(v.code == CsfCode.Ok); +} + +// **CSF-02** - the empty signature blob rejected. +test csf_02_empty_signature_rejected { + const empty_blob: [0]u8 = [0]u8{}; + const v = verify_commit_signature(GOOD_GID, 4, 7, GOOD_OPS, LEAF_KEY, &empty_blob, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.EmptySignature); +} + +// **CSF-03** - the all-zero signature blob rejected: the +// lazy-verifier trap. +test csf_03_zero_byte_signature_rejected { + const v = verify_commit_signature(GOOD_GID, 4, 7, GOOD_OPS, LEAF_KEY, &ZERO_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.EmptySignature); +} + +// **CSF-04** - the group identifier splice rejected: the +// transcript claims another group. +test csf_04_group_id_splice_rejected { + const v = verify_commit_signature(BAD_GID, 4, 7, GOOD_OPS, LEAF_KEY, &GOOD_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.GroupIdSplice); +} + +// **CSF-05** - the epoch mismatch rejected: the transcript +// claims three of a current four, the verdict carrying both. +test csf_05_epoch_mismatch_rejected { + const v = verify_commit_signature(GOOD_GID, 3, 7, GOOD_OPS, LEAF_KEY, &GOOD_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.EpochMismatch); + assert(v.current == 4); + assert(v.claimed == 3); +} + +// **CSF-06** - the committer not a current member +// rejected. +test csf_06_non_member_signer_rejected { + const v = verify_commit_signature(GOOD_GID, 4, 7, GOOD_OPS, LEAF_KEY, &GOOD_BLOB, GOOD_GID, 4, false, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.NonMemberSigner); +} + +// **CSF-07** - the stale signer key rejected: the +// pre-rotation key does not match the stored leaf-public. +test csf_07_stale_signer_key_rejected { + const v = verify_commit_signature(GOOD_GID, 4, 7, GOOD_OPS, STALE_KEY, &GOOD_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.StaleSignerKey); +} + +// **CSF-08** - the transcript ops hash mismatch rejected: +// the signature covers another commit body. +test csf_08_transcript_ops_hash_mismatch_rejected { + const v = verify_commit_signature(GOOD_GID, 4, 7, BAD_OPS, LEAF_KEY, &GOOD_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.TranscriptMismatch); +} + +// **CSF-09** - the check-order invariant: with every other +// field also wrong, the empty signature is still the +// answer. +test csf_09_check_order_empty_signature_first { + const empty_blob: [0]u8 = [0]u8{}; + const v = verify_commit_signature(BAD_GID, 99, 7, BAD_OPS, STALE_KEY, &empty_blob, GOOD_GID, 4, false, LEAF_KEY, GOOD_OPS); + assert(v.code == CsfCode.EmptySignature); +} + +// **CSF-10** - the green summary as a live call: the +// original's tenth test was a tautology (a count asserted +// against itself); this port pins the gate active with a +// live canonical pass instead, the WPS-10 precedent. +test csf_10_green_summary { + const v = canonical_call(); + assert(v.code == CsfCode.Ok); + assert(v.current == 0); + assert(v.claimed == 0); +} + +// The helpers have direct tests (coverage: Untested 0). +test bytes32_equal_answers { + assert(bytes32_equal(GOOD_GID, GOOD_GID)); + assert(!bytes32_equal(GOOD_GID, BAD_GID)); + assert(!bytes32_equal(LEAF_KEY, STALE_KEY)); + assert(!bytes32_equal(GOOD_OPS, BAD_OPS)); +} + +test all_zero_bytes_answers { + assert(all_zero_bytes(&ZERO_BLOB)); + assert(!all_zero_bytes(&GOOD_BLOB)); +} + +// Rule order is the original's fixed ladder, each probe +// stacking every later violation under the earlier one: the +// zero blob over all five others, the group splice over the +// four below it, the epoch over the three, the membership +// over the two, the stale key over the ops hash. +test rule_order_matches_original { + const v1 = verify_commit_signature(BAD_GID, 99, 7, BAD_OPS, STALE_KEY, &ZERO_BLOB, GOOD_GID, 4, false, LEAF_KEY, GOOD_OPS); + assert(v1.code == CsfCode.EmptySignature); + + const v2 = verify_commit_signature(BAD_GID, 99, 7, BAD_OPS, STALE_KEY, &GOOD_BLOB, GOOD_GID, 4, false, LEAF_KEY, GOOD_OPS); + assert(v2.code == CsfCode.GroupIdSplice); + + const v3 = verify_commit_signature(GOOD_GID, 99, 7, BAD_OPS, STALE_KEY, &GOOD_BLOB, GOOD_GID, 4, false, LEAF_KEY, GOOD_OPS); + assert(v3.code == CsfCode.EpochMismatch); + assert(v3.current == 4); + assert(v3.claimed == 99); + + const v4 = verify_commit_signature(GOOD_GID, 4, 7, BAD_OPS, STALE_KEY, &GOOD_BLOB, GOOD_GID, 4, false, LEAF_KEY, GOOD_OPS); + assert(v4.code == CsfCode.NonMemberSigner); + + const v5 = verify_commit_signature(GOOD_GID, 4, 7, BAD_OPS, STALE_KEY, &GOOD_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v5.code == CsfCode.StaleSignerKey); + + const v6 = verify_commit_signature(GOOD_GID, 4, 7, BAD_OPS, LEAF_KEY, &GOOD_BLOB, GOOD_GID, 4, true, LEAF_KEY, GOOD_OPS); + assert(v6.code == CsfCode.TranscriptMismatch); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/concurrent_add_remove.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/concurrent_add_remove.t27 new file mode 100644 index 0000000000..d39e8159cd --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/concurrent_add_remove.t27 @@ -0,0 +1,460 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-03::src::concurrent_add_remove; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-03/src/concurrent_add_remove.rs +// (CR-CHAT-03 - Concurrent Add/Remove ordering and ghost-member +// detection, Wave-20, L-CHAT-3-add, RFC 9420 12.2). +// +// Two members racing to commit against the same base epoch +// need a deterministic order: Updates first (rotate the +// keys of active leaves), then Removes (drop leaves), then +// Adds (fill the freed slots), ties broken by leaf index +// then hash id ascending. A wrong order leaves a ghost +// member, resurrects a removed leaf, hides which racing +// KeyPackage won, aborts a whole commit on a duplicate +// remove, or panics on a self-remove paired with an +// update. The original sorts the proposal set by the +// triple, rejects an exact duplicate sort key up front (a +// caller canonicalisation bug), then applies each proposal +// against the intermediate membership: an Update must find +// its leaf a member, a Remove must find it a member, an Add +// must find it absent - each violation answering with the +// leaf it names. +// +// The original returns Result. This port carries the verdict as a +// named struct (owner decision D5): Ok first, then the +// four variants in the original's declaration order - +// RemoveNonMember, AddExisting, UpdateNonMember, +// DuplicateSortKey - with every payload field: the error +// leaf, and on success the whole delta by value - the +// added, removed and updated lists in canonical +// application order and the final membership - each list a +// fixed capacity with its length, the final members a +// member array whose set semantics the tests read through +// a set-equality probe. +// +// The original's stable sort becomes a selection sort over +// a local copy of the proposals, ordered by a +// lexicographic before-probe over priority, leaf and hash +// id - the sorted result is the same permutation the +// original's sort produces, the tie key making it total. +// The BTreeSet of members becomes a fixed member array +// plus its length: contains a scan, insert an append, +// remove a shift-left compaction. The proposal enum +// becomes a kind enum plus one record form. + +// Canonical class priorities - smaller is earlier. +pub const PRI_UPDATE: u32 = 0; +pub const PRI_REMOVE: u32 = 1; +pub const PRI_ADD: u32 = 2; + +// The record capacity: every original fixture stays well +// under eight. +pub const CAR_CAP: usize = 8; + +// The proposal class. +pub enum ProposalKind { + Update, + Remove, + Add, +} + +// The proposal record: the class, the target leaf and the +// tie-break id. +pub struct ConcurrentProposalRec { + kind: ProposalKind, + leaf: u32, + hash_id: u64, +} + +// Verdict codes: Ok(MembershipDelta) and ConcurrencyError's variants, in declaration order. +pub enum CarCode { + Ok, + RemoveNonMember, + AddExisting, + UpdateNonMember, + DuplicateSortKey, +} + +// The verdict of the concurrent reconciliation: the error +// leaf, or the whole membership delta by value on success. +pub struct CarVerdict { + code: CarCode, + err_leaf: u32, + added: [8]u32, + added_len: usize, + removed: [8]u32, + removed_len: usize, + updated: [8]u32, + updated_len: usize, + final_members: [8]u32, + final_len: usize, +} + +// The class priority of a proposal - the original's +// priority() probe. +pub fn proposal_priority(p: ConcurrentProposalRec) -> u32 { + if (p.kind == ProposalKind.Update) { + return PRI_UPDATE; + } + if (p.kind == ProposalKind.Remove) { + return PRI_REMOVE; + } + return PRI_ADD; +} + +// Whether proposal a sorts strictly before proposal b - +// the original's (priority, leaf, hash_id) key compared +// lexicographically. +pub fn proposal_before(a: ConcurrentProposalRec, b: ConcurrentProposalRec) -> bool { + if (proposal_priority(a) != proposal_priority(b)) { + return proposal_priority(a) < proposal_priority(b); + } + if (a.leaf != b.leaf) { + return a.leaf < b.leaf; + } + return a.hash_id < b.hash_id; +} + +// Whether two proposals share the exact sort key - the +// original's duplicate-key collision. +pub fn sort_keys_equal(a: ConcurrentProposalRec, b: ConcurrentProposalRec) -> bool { + if (proposal_priority(a) != proposal_priority(b)) { + return false; + } + if (a.leaf != b.leaf) { + return false; + } + return a.hash_id == b.hash_id; +} + +// Whether the member array holds this leaf - the original's +// BTreeSet contains. +pub fn member_contains(members: [8]u32, members_len: usize, leaf: u32) -> bool { + for i in 0..members_len { + if (members[i] == leaf) { + return true; + } + } + return false; +} + +// [VERIFIED] Apply a set of concurrent proposals against the +// base membership in the canonical RFC 9420 12.2 order. +// Returns Ok with the whole delta only when every proposal +// is structurally valid against the intermediate state at +// the moment it is applied. +pub fn apply_concurrent(base_members: []const u32, proposals: []const ConcurrentProposalRec) -> CarVerdict { + var sorted: [8]ConcurrentProposalRec = [8]ConcurrentProposalRec{ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 0, hash_id: 0, },} + var sorted_len: usize = proposals.len; + for i in 0..proposals.len { + sorted[i] = proposals[i]; + } + for i in 0..sorted_len { + var best: usize = i; + for j in (i + 1)..sorted_len { + if (proposal_before(sorted[j], sorted[best])) { + best = j; + } + } + var tmp: ConcurrentProposalRec = sorted[i]; + sorted[i] = sorted[best]; + sorted[best] = tmp; + } + for i in 0..sorted_len { + if (i > 0) { + if (sort_keys_equal(sorted[i - 1], sorted[i])) { + return CarVerdict { code: CarCode.DuplicateSortKey, err_leaf: 0, added: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, added_len: 0, removed: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, removed_len: 0, updated: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, updated_len: 0, final_members: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, final_len: 0, }; + } + } + } + var members: [8]u32 = [8]u32{0, 0, 0, 0, 0, 0, 0, 0,} + var members_len: usize = base_members.len; + for i in 0..base_members.len { + members[i] = base_members[i]; + } + var added: [8]u32 = [8]u32{0, 0, 0, 0, 0, 0, 0, 0,} + var added_len: usize = 0; + var removed: [8]u32 = [8]u32{0, 0, 0, 0, 0, 0, 0, 0,} + var removed_len: usize = 0; + var updated: [8]u32 = [8]u32{0, 0, 0, 0, 0, 0, 0, 0,} + var updated_len: usize = 0; + for i in 0..sorted_len { + if (sorted[i].kind == ProposalKind.Update) { + if (!member_contains(members, members_len, sorted[i].leaf)) { + return CarVerdict { code: CarCode.UpdateNonMember, err_leaf: sorted[i].leaf, added: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, added_len: 0, removed: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, removed_len: 0, updated: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, updated_len: 0, final_members: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, final_len: 0, }; + } + updated[updated_len] = sorted[i].leaf; + updated_len = updated_len + 1; + } + if (sorted[i].kind == ProposalKind.Remove) { + if (!member_contains(members, members_len, sorted[i].leaf)) { + return CarVerdict { code: CarCode.RemoveNonMember, err_leaf: sorted[i].leaf, added: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, added_len: 0, removed: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, removed_len: 0, updated: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, updated_len: 0, final_members: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, final_len: 0, }; + } + var found_at: usize = 0; + for m in 0..members_len { + if (members[m] == sorted[i].leaf) { + found_at = m; + } + } + for m in found_at..(members_len - 1) { + members[m] = members[m + 1]; + } + members_len = members_len - 1; + removed[removed_len] = sorted[i].leaf; + removed_len = removed_len + 1; + } + if (sorted[i].kind == ProposalKind.Add) { + if (member_contains(members, members_len, sorted[i].leaf)) { + return CarVerdict { code: CarCode.AddExisting, err_leaf: sorted[i].leaf, added: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, added_len: 0, removed: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, removed_len: 0, updated: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, updated_len: 0, final_members: [8]u32{0, 0, 0, 0, 0, 0, 0, 0,}, final_len: 0, }; + } + members[members_len] = sorted[i].leaf; + members_len = members_len + 1; + added[added_len] = sorted[i].leaf; + added_len = added_len + 1; + } + } + return CarVerdict { code: CarCode.Ok, err_leaf: 0, added: added, added_len: added_len, removed: removed, removed_len: removed_len, updated: updated, updated_len: updated_len, final_members: members, final_len: members_len, }; +} + +// **CAR-01** - the add-after-remove ghost: with seven a +// member, the canonical order removes it first and the add +// re-adds it - the final set holds seven, no ghost. +test car_01_add_after_remove_ghost { + const base: [3]u32 = [3]u32{1, 2, 7,}; + const props: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 7, hash_id: 100, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 7, hash_id: 99, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.Ok); + assert(d.removed_len == 1); + assert(d.removed[0] == 7); + assert(d.added_len == 1); + assert(d.added[0] == 7); + assert(d.final_len == 3); + assert(member_contains(d.final_members, d.final_len, 1)); + assert(member_contains(d.final_members, d.final_len, 2)); + assert(member_contains(d.final_members, d.final_len, 7)); +} + +// **CAR-02** - the remove-after-add resurrection: the +// canonical order applies the remove first, and with seven +// absent from the base there is no leaf to remove. +test car_02_remove_after_add_resurrection { + const base: [2]u32 = [2]u32{1, 2,}; + const props: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 7, hash_id: 100, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 7, hash_id: 99, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.RemoveNonMember); + assert(d.err_leaf == 7); +} + +// **CAR-03** - the duplicate add: two adds of the same leaf +// with different tie ids - the second answers AddExisting. +test car_03_duplicate_add { + const base: [2]u32 = [2]u32{1, 2,}; + const props: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 9, hash_id: 1, }, + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 9, hash_id: 2, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.AddExisting); + assert(d.err_leaf == 9); +} + +// **CAR-04** - the duplicate remove: two removes of the +// same leaf - the second answers RemoveNonMember, the +// different tie ids keeping it out of the duplicate-key +// rule. +test car_04_duplicate_remove { + const base: [3]u32 = [3]u32{1, 2, 5,}; + const props: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 5, hash_id: 1, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 5, hash_id: 2, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.RemoveNonMember); + assert(d.err_leaf == 5); +} + +// **CAR-05** - the self-remove with update: the update +// applies first on the member, the remove then drops it - +// the commit succeeds and the leaf is gone. +test car_05_self_remove_with_update { + const base: [3]u32 = [3]u32{1, 2, 3,}; + const props: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 2, hash_id: 1, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 2, hash_id: 2, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.Ok); + assert(d.updated_len == 1); + assert(d.updated[0] == 2); + assert(d.removed_len == 1); + assert(d.removed[0] == 2); + assert(d.final_len == 2); + assert(member_contains(d.final_members, d.final_len, 1)); + assert(member_contains(d.final_members, d.final_len, 3)); + assert(!member_contains(d.final_members, d.final_len, 2)); +} + +// **CAR-06** - the empty proposal set: the delta carries +// no change and the final set equals the base. +test car_06_empty_proposal_set { + const base: [3]u32 = [3]u32{1, 2, 3,}; + const props: [0]ConcurrentProposalRec = [0]ConcurrentProposalRec{}; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.Ok); + assert(d.added_len == 0); + assert(d.removed_len == 0); + assert(d.updated_len == 0); + assert(d.final_len == 3); + assert(member_contains(d.final_members, d.final_len, 1)); + assert(member_contains(d.final_members, d.final_len, 2)); + assert(member_contains(d.final_members, d.final_len, 3)); +} + +// **CAR-07** - the order determinism: two permutations of +// one proposal set answer identical deltas. +test car_07_order_determinism { + const base: [2]u32 = [2]u32{1, 2,}; + const a: [3]ConcurrentProposalRec = [3]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 11, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 1, hash_id: 22, }, + ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 2, hash_id: 33, }, + }; + const b: [3]ConcurrentProposalRec = [3]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 2, hash_id: 33, }, + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 11, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 1, hash_id: 22, }, + }; + const d1 = apply_concurrent(&base, &a); + const d2 = apply_concurrent(&base, &b); + assert(d1.code == CarCode.Ok); + assert(d2.code == CarCode.Ok); + assert(d1.added_len == d2.added_len); + assert(d1.added[0] == d2.added[0]); + assert(d1.removed_len == d2.removed_len); + assert(d1.removed[0] == d2.removed[0]); + assert(d1.updated_len == d2.updated_len); + assert(d1.updated[0] == d2.updated[0]); + assert(d1.final_len == d2.final_len); + assert(member_contains(d1.final_members, d1.final_len, 2)); + assert(member_contains(d1.final_members, d1.final_len, 5)); + assert(member_contains(d2.final_members, d2.final_len, 2)); + assert(member_contains(d2.final_members, d2.final_len, 5)); +} + +// **CAR-08** - the tie-break by hash id: two adds of one +// leaf sort by the tie id, the lower one first - the +// second answers AddExisting. +test car_08_tie_break_by_hash_id { + const base: [0]u32 = [0]u32{}; + const props: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 99, }, + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.AddExisting); + assert(d.err_leaf == 5); +} + +// **CAR-09** - the duplicate sort key: the exact triple +// twice answers up front, before any proposal applies. +test car_09_duplicate_sort_key { + const base: [0]u32 = [0]u32{}; + const props: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.DuplicateSortKey); +} + +// **CAR-10** - the module reachable: the original's tenth +// test pinned the priority constants alone, and constant +// comparisons fold at compile time in the reference path - +// the port pins the gate live instead (the WPS-10 +// precedent): one add against an empty base answers Ok with +// the leaf added, the class priorities asserted alongside. +test car_10_green_each { + const base: [0]u32 = [0]u32{}; + const props: [1]ConcurrentProposalRec = [1]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 4, hash_id: 1, }, + }; + const d = apply_concurrent(&base, &props); + assert(d.code == CarCode.Ok); + assert(d.added_len == 1); + assert(d.added[0] == 4); + assert(d.final_len == 1); + assert(d.final_members[0] == 4); + assert(PRI_UPDATE == 0); + assert(PRI_REMOVE == 1); + assert(PRI_ADD == 2); +} + +// The helpers have direct tests (coverage: Untested 0). +test proposal_priority_answers { + assert(proposal_priority(ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 1, hash_id: 1, }) == PRI_UPDATE); + assert(proposal_priority(ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 1, hash_id: 1, }) == PRI_REMOVE); + assert(proposal_priority(ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 1, hash_id: 1, }) == PRI_ADD); +} + +test proposal_before_answers { + assert(proposal_before(ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 9, hash_id: 9, }, ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 1, hash_id: 1, })); + assert(!proposal_before(ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 1, hash_id: 1, }, ConcurrentProposalRec { kind: ProposalKind.Update, leaf: 9, hash_id: 9, })); + assert(proposal_before(ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 1, hash_id: 50, }, ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 1, hash_id: 99, })); + assert(proposal_before(ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 1, hash_id: 99, }, ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 2, hash_id: 1, })); +} + +test sort_keys_equal_answers { + assert(sort_keys_equal(ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, })); + assert(!sort_keys_equal(ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 2, })); + assert(!sort_keys_equal(ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 5, hash_id: 1, })); + assert(!sort_keys_equal(ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 6, hash_id: 1, })); +} + +test member_contains_answers { + var members: [8]u32 = [8]u32{0, 0, 0, 0, 0, 0, 0, 0,} + members[0] = 1; + members[1] = 2; + members[2] = 7; + assert(member_contains(members, 3, 7)); + assert(!member_contains(members, 3, 5)); + assert(!member_contains(members, 0, 1)); +} + +// Rule order is the original's: the duplicate-key rule +// answers before any proposal applies - a set carrying both +// a duplicate key and an apply-time violation answers the +// duplicate - and the sort runs before the apply - the +// same leaf's remove answers before its add under a base +// that holds it, and after its add under a base that does +// not. +test rule_order_matches_original { + const base: [2]u32 = [2]u32{1, 2,}; + const dup_and_bad: [3]ConcurrentProposalRec = [3]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 5, hash_id: 1, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 99, hash_id: 7, }, + }; + const v1 = apply_concurrent(&base, &dup_and_bad); + assert(v1.code == CarCode.DuplicateSortKey); + + const holds: [3]u32 = [3]u32{1, 2, 7,}; + const same_leaf: [2]ConcurrentProposalRec = [2]ConcurrentProposalRec{ + ConcurrentProposalRec { kind: ProposalKind.Add, leaf: 7, hash_id: 100, }, + ConcurrentProposalRec { kind: ProposalKind.Remove, leaf: 7, hash_id: 99, }, + }; + const v2 = apply_concurrent(&holds, &same_leaf); + assert(v2.code == CarCode.Ok); + assert(v2.final_len == 3); + assert(member_contains(v2.final_members, v2.final_len, 7)); + + const absent: [2]u32 = [2]u32{1, 2,}; + const v3 = apply_concurrent(&absent, &same_leaf); + assert(v3.code == CarCode.RemoveNonMember); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/psk_external_injection.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/psk_external_injection.t27 new file mode 100644 index 0000000000..7f99458f9f --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/psk_external_injection.t27 @@ -0,0 +1,385 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-03::src::psk_external_injection; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-03/src/psk_external_injection.rs +// (CR-CHAT-03 - MLS PSK external/resumption injection +// defense, Wave-26 Lane A, L-CHAT-3-psk, RFC 9420 +// 5.3/12.4.4). +// +// A smuggled Pre-Shared Key reference must never reach the +// key schedule: an External PSK the group never +// provisioned, a Resumption PSK spliced from another +// group or forward-referencing an epoch not yet accepted +// (a state-rollback primitive), a nonce of the wrong +// length, or a replay of a quad the ledger already +// consumed. The rules, in the original's fixed order: the +// canonical nonce length (thirty-two), the External +// identifier provisioned, the Resumption group equal to +// the local one, the Resumption epoch strictly earlier +// than the current one, and the (identifier, group, epoch, +// nonce) quad fresh. +// +// The original returns Result<(), PskInjectionError>. This +// port carries the verdict as a named struct (owner +// decision D5): Ok first, then the five variants in the +// original's declaration order - NonCanonicalNonceLength, +// UnprovisionedExternalId, ResumptionGroupSplice, +// ResumptionEpochRollback, NonceReplay. Every original +// variant carries no payload, so the code is the whole +// verdict. +// +// The original's reference and view structs become flat +// parameters, the byte vectors arriving as plain slices. +// The provisioned-identifier set becomes a slice of +// records holding the identifier with its length, the +// consumed-quad ledger a slice of records holding the +// identifier, the group identifier (possibly empty - the +// external form carries none), the epoch and the canonical +// nonce - both original BTreeSet contains answered by +// scans. + +// Canonical length of a PSK nonce (32 bytes). +pub const PSK_NONCE_LEN: usize = 32; + +// The PSK kind - the original's PskType in declaration +// order. +pub enum PskKind { + External, + Resumption, +} + +// A provisioned External PSK identifier. +pub struct InjectedPskId { + id: [32]u8, + id_len: usize, +} + +// A consumed (identifier, group, epoch, nonce) quad. +pub struct UsedNonce { + id: [32]u8, + id_len: usize, + gid: [32]u8, + gid_len: usize, + epoch: u64, + nonce: [32]u8, +} + +// Verdict codes: Ok(()) and PskInjectionError's variants, in declaration order. +pub enum PskInjCode { + Ok, + NonCanonicalNonceLength, + UnprovisionedExternalId, + ResumptionGroupSplice, + ResumptionEpochRollback, + NonceReplay, +} + +// The verdict of the PSK reference validation - the +// original's variants carry no payload, so the code alone +// carries it. +pub struct PskInjVerdict { + code: PskInjCode, +} + +// Content equality over two byte slices - the original's +// Vec equality on the group identifiers. +pub fn bytes_equal(a: []const u8, b: []const u8) -> bool { + if (a.len != b.len) { + return false; + } + for i in 0..a.len { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether the External identifier was provisioned - the +// original's BTreeSet contains, answered by a scan. +pub fn id_provisioned(provisioned: []const InjectedPskId, psk_id: []const u8) -> bool { + for i in 0..provisioned.len { + if (provisioned[i].id_len == psk_id.len) { + var same: bool = true; + for j in 0..psk_id.len { + if (provisioned[i].id[j] != psk_id[j]) { + same = false; + } + } + if (same) { + return true; + } + } + } + return false; +} + +// Whether this (identifier, group, epoch, nonce) quad was +// already consumed - the original's BTreeSet contains +// over the replay ledger, the nonce compared behind the +// canonical length rule one already guaranteed. +pub fn nonce_quad_used(used: []const UsedNonce, psk_id: []const u8, psk_group_id: []const u8, psk_epoch: u64, psk_nonce: []const u8) -> bool { + if (psk_nonce.len != 32) { + return false; + } + for i in 0..used.len { + if (used[i].id_len == psk_id.len) { + if (used[i].gid_len == psk_group_id.len) { + if (used[i].epoch == psk_epoch) { + var same: bool = true; + for j in 0..psk_id.len { + if (used[i].id[j] != psk_id[j]) { + same = false; + } + } + for j in 0..psk_group_id.len { + if (used[i].gid[j] != psk_group_id[j]) { + same = false; + } + } + for j in 0..32 { + if (used[i].nonce[j] != psk_nonce[j]) { + same = false; + } + } + if (same) { + return true; + } + } + } + } + } + return false; +} + +// [VERIFIED] Validate a PSK reference against the +// receiving group's view. Returns Ok only when every rule +// holds, checked in the original's fixed order: the +// canonical nonce length, the provisioned External +// identifier, the Resumption group, the strictly earlier +// Resumption epoch and the fresh quad. +pub fn validate_psk_ref(psk_type: PskKind, psk_id: []const u8, psk_group_id: []const u8, psk_epoch: u64, psk_nonce: []const u8, provisioned: []const InjectedPskId, local_group_id: []const u8, current_epoch: u64, used_nonces: []const UsedNonce) -> PskInjVerdict { + if (psk_nonce.len != PSK_NONCE_LEN) { + return PskInjVerdict { code: PskInjCode.NonCanonicalNonceLength, }; + } + if (psk_type == PskKind.External) { + if (!id_provisioned(provisioned, psk_id)) { + return PskInjVerdict { code: PskInjCode.UnprovisionedExternalId, }; + } + } + if (psk_type == PskKind.Resumption) { + if (!bytes_equal(psk_group_id, local_group_id)) { + return PskInjVerdict { code: PskInjCode.ResumptionGroupSplice, }; + } + if (psk_epoch >= current_epoch) { + return PskInjVerdict { code: PskInjCode.ResumptionEpochRollback, }; + } + } + if (nonce_quad_used(used_nonces, psk_id, psk_group_id, psk_epoch, psk_nonce)) { + return PskInjVerdict { code: PskInjCode.NonceReplay, }; + } + return PskInjVerdict { code: PskInjCode.Ok, }; +} + +// The fixture bytes: the local group of double-A-hex, the +// stranger's group of double-B-hex, the identifier labels +// of the fixtures, and the nonces of the per-fixture +// bytes. +pub const LOCAL_GID: [32]u8 = [32]u8{170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,} +pub const OTHER_GID: [32]u8 = [32]u8{187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187,} +pub const TEAM_ALPHA: [10]u8 = [10]u8{116, 101, 97, 109, 45, 97, 108, 112, 104, 97,} +pub const TEAM: [4]u8 = [4]u8{116, 101, 97, 109,} +pub const TEAM_EVIL: [9]u8 = [9]u8{116, 101, 97, 109, 45, 101, 118, 105, 108,} +pub const RESUME7: [8]u8 = [8]u8{114, 101, 115, 117, 109, 101, 45, 55,} +pub const RESUME: [6]u8 = [6]u8{114, 101, 115, 117, 109, 101,} +pub const N11: [32]u8 = [32]u8{17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,} +pub const N22: [32]u8 = [32]u8{34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,} +pub const N44: [32]u8 = [32]u8{68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,} +pub const N55: [32]u8 = [32]u8{85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,} +pub const N66: [32]u8 = [32]u8{102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,} +pub const N77: [32]u8 = [32]u8{119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119,} +pub const N88: [32]u8 = [32]u8{136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136,} + +// **PSK-01** - the valid External PSK with the provisioned +// identifier accepted. +test psk_01_valid_external_accepted { + const empty_gid: [0]u8 = [0]u8{}; + const provisioned: [1]InjectedPskId = [1]InjectedPskId{ + InjectedPskId { id: [32]u8{116, 101, 97, 109, 45, 97, 108, 112, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 10, }, + }; + const none: [0]UsedNonce = [0]UsedNonce{}; + const v = validate_psk_ref(PskKind.External, &TEAM_ALPHA, &empty_gid, 0, &N11, &provisioned, &LOCAL_GID, 5, &none); + assert(v.code == PskInjCode.Ok); +} + +// **PSK-02** - the valid Resumption PSK chaining from the +// earlier epoch accepted. +test psk_02_valid_resumption_accepted { + const empty_provisioned: [0]InjectedPskId = [0]InjectedPskId{}; + const none: [0]UsedNonce = [0]UsedNonce{}; + const v = validate_psk_ref(PskKind.Resumption, &RESUME7, &LOCAL_GID, 7, &N22, &empty_provisioned, &LOCAL_GID, 10, &none); + assert(v.code == PskInjCode.Ok); +} + +// **PSK-03** - the thirty-one-byte nonce rejected. +test psk_03_short_nonce_rejected { + const empty_gid: [0]u8 = [0]u8{}; + const provisioned: [1]InjectedPskId = [1]InjectedPskId{ + InjectedPskId { id: [32]u8{116, 101, 97, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 4, }, + }; + const none: [0]UsedNonce = [0]UsedNonce{}; + var short_nonce: [31]u8; + for j in 0..31 { + short_nonce[j] = 51; + } + const v = validate_psk_ref(PskKind.External, &TEAM, &empty_gid, 0, &short_nonce, &provisioned, &LOCAL_GID, 5, &none); + assert(v.code == PskInjCode.NonCanonicalNonceLength); +} + +// **PSK-04** - the empty nonce rejected before any other +// check. +test psk_04_empty_nonce_rejected { + const empty_gid: [0]u8 = [0]u8{}; + const provisioned: [1]InjectedPskId = [1]InjectedPskId{ + InjectedPskId { id: [32]u8{116, 101, 97, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 4, }, + }; + const none: [0]UsedNonce = [0]UsedNonce{}; + const empty_nonce: [0]u8 = [0]u8{}; + const v = validate_psk_ref(PskKind.External, &TEAM, &empty_gid, 0, &empty_nonce, &provisioned, &LOCAL_GID, 5, &none); + assert(v.code == PskInjCode.NonCanonicalNonceLength); +} + +// **PSK-05** - the unprovisioned External identifier +// rejected. +test psk_05_unprovisioned_external_rejected { + const empty_gid: [0]u8 = [0]u8{}; + const provisioned: [1]InjectedPskId = [1]InjectedPskId{ + InjectedPskId { id: [32]u8{116, 101, 97, 109, 45, 97, 108, 112, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 10, }, + }; + const none: [0]UsedNonce = [0]UsedNonce{}; + const v = validate_psk_ref(PskKind.External, &TEAM_EVIL, &empty_gid, 0, &N44, &provisioned, &LOCAL_GID, 5, &none); + assert(v.code == PskInjCode.UnprovisionedExternalId); +} + +// **PSK-06** - the Resumption group splice rejected: the +// stranger's group. +test psk_06_resumption_group_splice_rejected { + const empty_provisioned: [0]InjectedPskId = [0]InjectedPskId{}; + const none: [0]UsedNonce = [0]UsedNonce{}; + const v = validate_psk_ref(PskKind.Resumption, &RESUME7, &OTHER_GID, 7, &N55, &empty_provisioned, &LOCAL_GID, 10, &none); + assert(v.code == PskInjCode.ResumptionGroupSplice); +} + +// **PSK-07** - the Resumption epoch equal to the current +// one rejected: the reference must chain strictly earlier. +test psk_07_resumption_forward_epoch_rejected { + const empty_provisioned: [0]InjectedPskId = [0]InjectedPskId{}; + const none: [0]UsedNonce = [0]UsedNonce{}; + const v = validate_psk_ref(PskKind.Resumption, &RESUME7, &LOCAL_GID, 10, &N66, &empty_provisioned, &LOCAL_GID, 10, &none); + assert(v.code == PskInjCode.ResumptionEpochRollback); +} + +// **PSK-08** - the Resumption epoch past the current one +// rejected. +test psk_08_resumption_epoch_overflow_rejected { + const empty_provisioned: [0]InjectedPskId = [0]InjectedPskId{}; + const none: [0]UsedNonce = [0]UsedNonce{}; + const v = validate_psk_ref(PskKind.Resumption, &RESUME7, &LOCAL_GID, 11, &N66, &empty_provisioned, &LOCAL_GID, 10, &none); + assert(v.code == PskInjCode.ResumptionEpochRollback); +} + +// **PSK-09** - the replayed quad rejected: this very +// (identifier, empty group, epoch, nonce) was consumed +// before. +test psk_09_replayed_nonce_rejected { + const empty_gid: [0]u8 = [0]u8{}; + const provisioned: [1]InjectedPskId = [1]InjectedPskId{ + InjectedPskId { id: [32]u8{116, 101, 97, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 4, }, + }; + const used: [1]UsedNonce = [1]UsedNonce{ + UsedNonce { id: [32]u8{116, 101, 97, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 4, gid: [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, gid_len: 0, epoch: 0, nonce: N77, }, + }; + const v = validate_psk_ref(PskKind.External, &TEAM, &empty_gid, 0, &N77, &provisioned, &LOCAL_GID, 5, &used); + assert(v.code == PskInjCode.NonceReplay); +} + +// **PSK-10** - the same nonce at a different epoch fresh: +// the replay ledger keys on the whole quad, never the +// nonce alone. +test psk_10_same_nonce_different_epoch_accepted { + const empty_provisioned: [0]InjectedPskId = [0]InjectedPskId{}; + const used: [1]UsedNonce = [1]UsedNonce{ + UsedNonce { id: [32]u8{114, 101, 115, 117, 109, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 6, gid: LOCAL_GID, gid_len: 32, epoch: 7, nonce: N88, }, + }; + const v = validate_psk_ref(PskKind.Resumption, &RESUME, &LOCAL_GID, 8, &N88, &empty_provisioned, &LOCAL_GID, 10, &used); + assert(v.code == PskInjCode.Ok); +} + +// The helpers have direct tests (coverage: Untested 0). +test bytes_equal_answers { + assert(bytes_equal(&LOCAL_GID, &LOCAL_GID)); + assert(!bytes_equal(&LOCAL_GID, &OTHER_GID)); + assert(!bytes_equal(&TEAM, &TEAM_ALPHA)); +} + +test id_provisioned_answers { + const provisioned: [1]InjectedPskId = [1]InjectedPskId{ + InjectedPskId { id: [32]u8{116, 101, 97, 109, 45, 97, 108, 112, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 10, }, + }; + assert(id_provisioned(&provisioned, &TEAM_ALPHA)); + assert(!id_provisioned(&provisioned, &TEAM_EVIL)); + const empty: [0]InjectedPskId = [0]InjectedPskId{}; + assert(!id_provisioned(&empty, &TEAM_ALPHA)); +} + +test nonce_quad_used_answers { + const empty_gid: [0]u8 = [0]u8{}; + const used: [1]UsedNonce = [1]UsedNonce{ + UsedNonce { id: [32]u8{116, 101, 97, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 4, gid: [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, gid_len: 0, epoch: 0, nonce: N77, }, + }; + assert(nonce_quad_used(&used, &TEAM, &empty_gid, 0, &N77)); + assert(!nonce_quad_used(&used, &TEAM, &empty_gid, 1, &N77)); + assert(!nonce_quad_used(&used, &TEAM, &empty_gid, 0, &N88)); + assert(!nonce_quad_used(&used, &TEAM_ALPHA, &empty_gid, 0, &N77)); + var short_nonce: [31]u8; + for j in 0..31 { + short_nonce[j] = 119; + } + assert(!nonce_quad_used(&used, &TEAM, &empty_gid, 0, &short_nonce)); +} + +// Rule order is the original's fixed ladder, each probe +// stacking the earlier rule's violation on the later ones +// - the consumed quad riding under every earlier rule. +test rule_order_matches_original { + const empty_gid: [0]u8 = [0]u8{}; + const empty_provisioned: [0]InjectedPskId = [0]InjectedPskId{}; + const used: [1]UsedNonce = [1]UsedNonce{ + UsedNonce { id: [32]u8{116, 101, 97, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 4, gid: [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, gid_len: 0, epoch: 0, nonce: N77, }, + }; + var short_nonce: [31]u8; + for j in 0..31 { + short_nonce[j] = 119; + } + const empty_nonce: [0]u8 = [0]u8{}; + + const v1 = validate_psk_ref(PskKind.Resumption, &RESUME7, &OTHER_GID, 11, &short_nonce, &empty_provisioned, &LOCAL_GID, 10, &used); + assert(v1.code == PskInjCode.NonCanonicalNonceLength); + + const v2 = validate_psk_ref(PskKind.Resumption, &RESUME7, &OTHER_GID, 11, &empty_nonce, &empty_provisioned, &LOCAL_GID, 10, &used); + assert(v2.code == PskInjCode.NonCanonicalNonceLength); + + const v3 = validate_psk_ref(PskKind.External, &TEAM_EVIL, &empty_gid, 0, &N44, &empty_provisioned, &LOCAL_GID, 5, &used); + assert(v3.code == PskInjCode.UnprovisionedExternalId); + + const v4 = validate_psk_ref(PskKind.Resumption, &RESUME7, &OTHER_GID, 11, &N55, &empty_provisioned, &LOCAL_GID, 10, &used); + assert(v4.code == PskInjCode.ResumptionGroupSplice); + + const v5 = validate_psk_ref(PskKind.Resumption, &RESUME7, &LOCAL_GID, 11, &N66, &empty_provisioned, &LOCAL_GID, 10, &used); + assert(v5.code == PskInjCode.ResumptionEpochRollback); + + const provisioned: [1]InjectedPskId = [1]InjectedPskId{ + InjectedPskId { id: [32]u8{116, 101, 97, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}, id_len: 4, }, + }; + const v6 = validate_psk_ref(PskKind.External, &TEAM, &empty_gid, 0, &N77, &provisioned, &LOCAL_GID, 5, &used); + assert(v6.code == PskInjCode.NonceReplay); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_blank_node_depth_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_blank_node_depth_guard.t27 new file mode 100644 index 0000000000..394a73f9f5 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_blank_node_depth_guard.t27 @@ -0,0 +1,260 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_blank_node_depth_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_blank_node_depth_guard.rs +// (CR-CHAT-03 - Ratchet tree blank node depth guard, Wave-75 +// Lane A, R-CHAT-2). +// +// TreeKEM resolution skips blank nodes, and a deep run of +// consecutive blanks from a leaf toward the root leaves a +// subtree unresolvable - no member can encrypt to it, the +// resolution collapses to an empty set, and an update +// path that cannot encrypt through the chain blocks +// entirely. The gate, in the original's fixed order: the +// tree non-empty, then per node the depth ceiling +// (sixteen), the never-blank leaf probe, and the +// consecutive-blank run with its ceiling (four) - a +// non-blank node resets the run - and after the walk the +// root must not be blank. The prose lists a +// non-blank-after-chain rule and the enum declares +// NonBlankAfterBlank, but no code path ever answers it - +// the run reset is silent; the variant stays in the enum, +// declared and unreachable exactly as in the original. +// +// The original returns Result<(), BlankDepthError>. This +// port carries the verdict as a named struct (owner +// decision D5): Ok first, then the six variants in the +// original's declaration order - BlankTooDeep, +// BlankAtRoot, MaxDepthExceeded, LeafIsBlank, TreeEmpty, +// NonBlankAfterBlank - with every payload field: the +// measured run of the too-deep rule. + +// Maximum consecutive blank node depth. +pub const RBND_MAX_BLANK_DEPTH: usize = 4; + +// Maximum tree depth. +pub const RBND_MAX_TREE_DEPTH: usize = 16; + +// A node in the ratchet tree. +pub struct BlankDepthTreeNode { + blank: bool, + depth: usize, + leaf: bool, +} + +// Verdict codes: Ok(()) and BlankDepthError's variants, in declaration order. +pub enum RbndCode { + Ok, + BlankTooDeep, + BlankAtRoot, + MaxDepthExceeded, + LeafIsBlank, + TreeEmpty, + NonBlankAfterBlank, +} + +// The verdict of the blank-depth validation, with the +// measured run of the too-deep rule. +pub struct RbndVerdict { + code: RbndCode, + run: usize, +} + +// [VERIFIED] Validate that blank nodes do not exceed the +// allowed depth. Returns Ok only when every rule holds, +// checked in the original's fixed order: the non-empty +// tree, then per node the depth ceiling, the leaf probe +// and the consecutive-blank run, then the resolved root. +pub fn validate_blank_node_depth(nodes: []const BlankDepthTreeNode) -> RbndVerdict { + if (nodes.len == 0) { + return RbndVerdict { code: RbndCode.TreeEmpty, run: 0, }; + } + var current_blank_run: usize = 0; + for i in 0..nodes.len { + if (nodes[i].depth > RBND_MAX_TREE_DEPTH) { + return RbndVerdict { code: RbndCode.MaxDepthExceeded, run: 0, }; + } + if (nodes[i].leaf) { + if (nodes[i].blank) { + return RbndVerdict { code: RbndCode.LeafIsBlank, run: 0, }; + } + } + if (nodes[i].blank) { + current_blank_run = current_blank_run + 1; + if (current_blank_run > RBND_MAX_BLANK_DEPTH) { + return RbndVerdict { code: RbndCode.BlankTooDeep, run: current_blank_run, }; + } + } else { + current_blank_run = 0; + } + } + if (nodes[0].blank) { + return RbndVerdict { code: RbndCode.BlankAtRoot, run: 0, }; + } + return RbndVerdict { code: RbndCode.Ok, run: 0, }; +} + +// **RBND-01** - the blank chain too deep rejected: a run +// of five over the ceiling of four, the verdict carrying +// the measured run. +test rbnd_01_too_deep_rejected { + const tree: [6]BlankDepthTreeNode = [6]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 1, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 2, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 3, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 4, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 5, leaf: false, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.BlankTooDeep); + assert(v.run == 5); +} + +// **RBND-02** - the blank root rejected - after the walk, +// exactly as the original checks it last. +test rbnd_02_blank_root_rejected { + const tree: [3]BlankDepthTreeNode = [3]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: true, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 1, leaf: true, }, + BlankDepthTreeNode { blank: false, depth: 2, leaf: true, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.BlankAtRoot); +} + +// **RBND-03** - the tree too deep rejected: a node at +// depth seventeen of the ceiling of sixteen. +test rbnd_03_too_deep_rejected { + const tree: [2]BlankDepthTreeNode = [2]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 17, leaf: false, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.MaxDepthExceeded); +} + +// **RBND-04** - the blank leaf rejected: a leaf carrying +// the blank form. +test rbnd_04_leaf_blank_rejected { + const tree: [2]BlankDepthTreeNode = [2]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 1, leaf: true, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.LeafIsBlank); +} + +// **RBND-05** - the empty tree rejected. +test rbnd_05_empty_rejected { + const tree: [0]BlankDepthTreeNode = [0]BlankDepthTreeNode{}; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.TreeEmpty); +} + +// **RBND-06** - the valid tree accepted: one blank inside +// the ceiling, resolved at both ends. +test rbnd_06_valid_accepted { + const tree: [5]BlankDepthTreeNode = [5]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 1, leaf: true, }, + BlankDepthTreeNode { blank: true, depth: 2, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 3, leaf: true, }, + BlankDepthTreeNode { blank: false, depth: 4, leaf: true, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.Ok); +} + +// **RBND-07** - the maximum blank depth accepted: a run of +// exactly four, resolved before the root. +test rbnd_07_max_blank_accepted { + const tree: [6]BlankDepthTreeNode = [6]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 1, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 2, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 3, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 4, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 5, leaf: true, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.Ok); +} + +// **RBND-08** - the tree with no blanks accepted. +test rbnd_08_no_blanks_accepted { + const tree: [3]BlankDepthTreeNode = [3]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 1, leaf: true, }, + BlankDepthTreeNode { blank: false, depth: 2, leaf: true, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.Ok); +} + +// **RBND-09** - the single resolved root accepted. +test rbnd_09_single_root_accepted { + const tree: [1]BlankDepthTreeNode = [1]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.Ok); +} + +// **RBND-10** - the chain reset after a non-blank +// accepted: two runs of two, each inside the ceiling. +test rbnd_10_chain_reset_accepted { + const tree: [7]BlankDepthTreeNode = [7]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 1, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 2, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 3, leaf: true, }, + BlankDepthTreeNode { blank: true, depth: 4, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 5, leaf: false, }, + BlankDepthTreeNode { blank: false, depth: 6, leaf: true, }, + }; + const v = validate_blank_node_depth(&tree); + assert(v.code == RbndCode.Ok); +} + +// Rule order is the original's fixed ladder: the emptiness +// first, then per node the depth ceiling over the leaf +// probe over the run probe, and the run probe over the +// root check - a five-blank chain ending in a blank root +// answers the run rule, never the root rule. +test rule_order_matches_original { + const empty: [0]BlankDepthTreeNode = [0]BlankDepthTreeNode{}; + const v1 = validate_blank_node_depth(&empty); + assert(v1.code == RbndCode.TreeEmpty); + + const stacked: [3]BlankDepthTreeNode = [3]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: true, depth: 17, leaf: true, }, + BlankDepthTreeNode { blank: true, depth: 17, leaf: true, }, + BlankDepthTreeNode { blank: true, depth: 17, leaf: true, }, + }; + const v2 = validate_blank_node_depth(&stacked); + assert(v2.code == RbndCode.MaxDepthExceeded); + + const blank_leaf: [2]BlankDepthTreeNode = [2]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: false, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 1, leaf: true, }, + }; + const v3 = validate_blank_node_depth(&blank_leaf); + assert(v3.code == RbndCode.LeafIsBlank); + + const deep_root: [5]BlankDepthTreeNode = [5]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: true, depth: 0, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 1, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 2, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 3, leaf: false, }, + BlankDepthTreeNode { blank: true, depth: 4, leaf: false, }, + }; + const v4 = validate_blank_node_depth(&deep_root); + assert(v4.code == RbndCode.BlankTooDeep); + assert(v4.run == 5); + + const root_only: [1]BlankDepthTreeNode = [1]BlankDepthTreeNode{ + BlankDepthTreeNode { blank: true, depth: 0, leaf: false, }, + }; + const v5 = validate_blank_node_depth(&root_only); + assert(v5.code == RbndCode.BlankAtRoot); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_alignment_uniformity_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_alignment_uniformity_guard.t27 new file mode 100644 index 0000000000..c4e62b1416 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_alignment_uniformity_guard.t27 @@ -0,0 +1,348 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_alignment_uniformity_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_block_alignment_uniformity_guard.rs +// (CR-CHAT-04 - Padding block alignment uniformity guard, Wave-158 +// Lane A, requirement R-CHAT-4). +// +// Padded blocks must share one alignment: varying alignment +// reveals where the payload ends and the padding begins, +// fingerprints size classes, and leaks the payload size +// distribution over many messages. The rules, in the original's +// order: the batch ceiling, then per block the zero-id probe, +// the repeated-id scan, the alignment floor and the power-of-two +// probe, and finally the uniformity check against the first +// block's alignment. +// +// The original returns Result<(), AlignmentUniformityError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the six error variants in the original's +// declaration order - NonUniform, ZeroBlockId, DuplicateBlockId, +// NotPowerOfTwo, BelowMinimum, TooMany - with every payload field: +// idx, expected and found for NonUniform, idx for the id rules, +// idx and got for NotPowerOfTwo, idx, got and min for +// BelowMinimum, and got and max for the batch ceiling. +// +// The original's Option first-alignment becomes the +// sentinel zero: the floor rule runs before the uniformity check, +// so every tracked alignment is at least sixteen and the sentinel +// never collides. The original's is_power_of_two becomes a +// halving loop - sixty-four iterations cover every usize, the +// residue one marking the powers. The BTreeSet dedup becomes a +// scan over the blocks already visited. + +// Maximum blocks per batch. +pub const PBAU_MAX_BLOCKS: usize = 1024; + +// Minimum alignment (bytes). +pub const PBAU_MIN_ALIGNMENT: usize = 16; + +// Block ID length. +pub const PBAU_BLOCK_ID_LEN: usize = 16; + +// A padding block alignment record. +pub struct AlignmentRecord { + block_id: [16]u8, + alignment: usize, +} + +// Verdict codes: Ok(()) and AlignmentUniformityError's variants, in declaration order. +pub enum AlignmentUniformityCode { + Ok, + NonUniform, + ZeroBlockId, + DuplicateBlockId, + NotPowerOfTwo, + BelowMinimum, + TooMany, +} + +// The verdict of the alignment uniformity validation, with every +// error payload field. +pub struct AlignmentUniformityVerdict { + code: AlignmentUniformityCode, + idx: usize, + got: usize, + bound: usize, +} + +// Whole-form equality over the fixed sixteen block-id bytes. +pub fn block_ids_equal(a: [16]u8, b: [16]u8) -> bool { + for i in 0..16 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero block id - the original's [0u8; 16] probe. +pub fn block_id_zero(id: [16]u8) -> bool { + for i in 0..16 { + if (id[i] != 0) { + return false; + } + } + return true; +} + +// Whether block i repeats an earlier id - the original's +// BTreeSet insert probe, answered by a scan. +pub fn block_seen_before(blocks: []const AlignmentRecord, i: usize) -> bool { + for j in 0..i { + if (block_ids_equal(blocks[j].block_id, blocks[i].block_id)) { + return true; + } + } + return false; +} + +// Whether the alignment is a power of two - the original's +// is_power_of_two, answered by halving: sixty-four iterations +// cover every usize, and only the powers halve down to one. +pub fn is_power_of_two(a: usize) -> bool { + var v: usize = a; + for _ in 0..64 { + if (v % 2 == 0) { + v = v / 2; + } + } + return v == 1; +} + +// [VERIFIED] Validate padding block alignment uniformity. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, then per block the zero +// id, the repeated id, the alignment floor and the power-of-two +// probe, then the uniformity check against the first alignment. +pub fn validate_alignment_uniformity(blocks: []const AlignmentRecord) -> AlignmentUniformityVerdict { + if (blocks.len > PBAU_MAX_BLOCKS) { + return AlignmentUniformityVerdict { code: AlignmentUniformityCode.TooMany, idx: 0, got: blocks.len, bound: PBAU_MAX_BLOCKS, }; + } + var first_alignment: usize = 0; + for i in 0..blocks.len { + if (block_id_zero(blocks[i].block_id)) { + return AlignmentUniformityVerdict { code: AlignmentUniformityCode.ZeroBlockId, idx: i, got: 0, bound: 0, }; + } + if (block_seen_before(blocks, i)) { + return AlignmentUniformityVerdict { code: AlignmentUniformityCode.DuplicateBlockId, idx: i, got: 0, bound: 0, }; + } + if (blocks[i].alignment < PBAU_MIN_ALIGNMENT) { + return AlignmentUniformityVerdict { code: AlignmentUniformityCode.BelowMinimum, idx: i, got: blocks[i].alignment, bound: PBAU_MIN_ALIGNMENT, }; + } + if (!is_power_of_two(blocks[i].alignment)) { + return AlignmentUniformityVerdict { code: AlignmentUniformityCode.NotPowerOfTwo, idx: i, got: blocks[i].alignment, bound: 0, }; + } + if (first_alignment == 0) { + first_alignment = blocks[i].alignment; + } else { + if (blocks[i].alignment != first_alignment) { + return AlignmentUniformityVerdict { code: AlignmentUniformityCode.NonUniform, idx: i, got: blocks[i].alignment, bound: first_alignment, }; + } + } + } + return AlignmentUniformityVerdict { code: AlignmentUniformityCode.Ok, idx: 0, got: 0, bound: 0, }; +} + +// The block ids of the fixtures, one byte repeated sixteen +// times - the original's bid(byte) helper, entry by entry. +pub const BID_01: [16]u8 = [16]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const BID_02: [16]u8 = [16]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const BID_03: [16]u8 = [16]u8{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; +pub const ZERO_BID: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The leading id bytes of the many-uniform fixture. +pub const PBAU_ID_BYTES_50: [50]u8 = [50]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,}; + +// The two id bytes of the too-many fixture: the original writes +// the big-endian u64 of i + 1 into id[0..8], and for every value +// in 1..=1025 only the last two of those eight bytes can carry a +// nonzero digit. +pub const PBAU_ID6_1025: [1025]u8 = [1025]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4,}; +pub const PBAU_ID7_1025: [1025]u8 = [1025]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1,}; + +// **PBAU-01** - non-uniform rejected: the second block aligns to +// thirty-two where the first set sixteen. +test pbau_01_non_uniform_rejected { + const blocks: [2]AlignmentRecord = [2]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 16, }, + AlignmentRecord { block_id: BID_02, alignment: 32, }, + }; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.NonUniform); + assert(v.idx == 1); + assert(v.bound == 16); + assert(v.got == 32); +} + +// **PBAU-02** - zero block id rejected. +test pbau_02_zero_id_rejected { + const blocks: [1]AlignmentRecord = [1]AlignmentRecord{ + AlignmentRecord { block_id: ZERO_BID, alignment: 16, }, + }; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.ZeroBlockId); + assert(v.idx == 0); +} + +// **PBAU-03** - duplicate block id rejected: the second block +// repeats the first's id. +test pbau_03_duplicate_rejected { + const blocks: [2]AlignmentRecord = [2]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 16, }, + AlignmentRecord { block_id: BID_01, alignment: 16, }, + }; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.DuplicateBlockId); + assert(v.idx == 1); +} + +// **PBAU-04** - not a power of two rejected: twenty-four. +test pbau_04_not_power_of_two_rejected { + const blocks: [1]AlignmentRecord = [1]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 24, }, + }; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.NotPowerOfTwo); + assert(v.idx == 0); + assert(v.got == 24); +} + +// **PBAU-05** - below the minimum rejected: eight of a floor +// sixteen. +test pbau_05_below_min_rejected { + const blocks: [1]AlignmentRecord = [1]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 8, }, + }; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.BelowMinimum); + assert(v.idx == 0); + assert(v.got == 8); + assert(v.bound == 16); +} + +// **PBAU-06** - too many rejected: 1025 blocks of a maximum +// 1024, built in a loop like the original's, each id carrying +// the big-endian u64 of its index plus one. The batch rule fires +// before any block is examined. +test pbau_06_too_many_rejected { + var blocks: [1025]AlignmentRecord; + for i in 0..1025 { + var id: [16]u8; + for j in 0..16 { + id[j] = 0; + } + id[6] = PBAU_ID6_1025[i]; + id[7] = PBAU_ID7_1025[i]; + blocks[i] = AlignmentRecord { block_id: id, alignment: 16, }; + } + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.TooMany); + assert(v.got == 1025); + assert(v.bound == 1024); +} + +// **PBAU-07** - the valid batch accepted: three blocks aligned +// to sixteen. +test pbau_07_valid_accepted { + const blocks: [3]AlignmentRecord = [3]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 16, }, + AlignmentRecord { block_id: BID_02, alignment: 16, }, + AlignmentRecord { block_id: BID_03, alignment: 16, }, + }; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.Ok); +} + +// **PBAU-08** - an empty batch accepted. +test pbau_08_empty_accepted { + const blocks: [0]AlignmentRecord = [0]AlignmentRecord{}; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.Ok); +} + +// **PBAU-09** - a single block accepted, alignment sixty-four. +test pbau_09_single_accepted { + const blocks: [1]AlignmentRecord = [1]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 64, }, + }; + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.Ok); +} + +// **PBAU-10** - many uniform blocks accepted: fifty blocks all +// aligned to thirty-two. +test pbau_10_many_uniform_accepted { + var blocks: [50]AlignmentRecord; + for i in 0..50 { + var id: [16]u8; + for j in 0..16 { + id[j] = 0; + } + id[0] = PBAU_ID_BYTES_50[i]; + blocks[i] = AlignmentRecord { block_id: id, alignment: 32, }; + } + const v = validate_alignment_uniformity(&blocks); + assert(v.code == AlignmentUniformityCode.Ok); +} + +// The four helpers have direct tests (coverage: Untested 0). +test pbau_helpers_answer { + assert(block_ids_equal(BID_01, BID_01)); + assert(!block_ids_equal(BID_01, BID_02)); + assert(block_id_zero(ZERO_BID)); + assert(!block_id_zero(BID_01)); + const three: [3]AlignmentRecord = [3]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 16, }, + AlignmentRecord { block_id: BID_02, alignment: 16, }, + AlignmentRecord { block_id: BID_01, alignment: 16, }, + }; + assert(!block_seen_before(&three, 0)); + assert(!block_seen_before(&three, 1)); + assert(block_seen_before(&three, 2)); + assert(is_power_of_two(1)); + assert(is_power_of_two(2)); + assert(is_power_of_two(16)); + assert(is_power_of_two(1024)); + assert(!is_power_of_two(0)); + assert(!is_power_of_two(24)); + assert(!is_power_of_two(48)); + assert(!is_power_of_two(18446744073709551615)); +} + +// Rule order is the original's: the zero id before the duplicate, +// the duplicate before the alignment floor, the floor before the +// power-of-two probe, and the power probe before the uniformity +// check. +test rule_order_matches_original { + const zero_twins: [2]AlignmentRecord = [2]AlignmentRecord{ + AlignmentRecord { block_id: ZERO_BID, alignment: 16, }, + AlignmentRecord { block_id: ZERO_BID, alignment: 16, }, + }; + const v1 = validate_alignment_uniformity(&zero_twins); + assert(v1.code == AlignmentUniformityCode.ZeroBlockId); + assert(v1.idx == 0); + + const dup_low: [2]AlignmentRecord = [2]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 16, }, + AlignmentRecord { block_id: BID_01, alignment: 8, }, + }; + const v2 = validate_alignment_uniformity(&dup_low); + assert(v2.code == AlignmentUniformityCode.DuplicateBlockId); + assert(v2.idx == 1); + + const floor_non_power: [1]AlignmentRecord = [1]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 12, }, + }; + const v3 = validate_alignment_uniformity(&floor_non_power); + assert(v3.code == AlignmentUniformityCode.BelowMinimum); + assert(v3.got == 12); + + const power_before_uniform: [2]AlignmentRecord = [2]AlignmentRecord{ + AlignmentRecord { block_id: BID_01, alignment: 32, }, + AlignmentRecord { block_id: BID_02, alignment: 24, }, + }; + const v4 = validate_alignment_uniformity(&power_before_uniform); + assert(v4.code == AlignmentUniformityCode.NotPowerOfTwo); + assert(v4.idx == 1); + assert(v4.got == 24); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_cipher_mode_uniformity_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_cipher_mode_uniformity_guard.t27 new file mode 100644 index 0000000000..b2781f0433 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_cipher_mode_uniformity_guard.t27 @@ -0,0 +1,363 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_block_cipher_mode_uniformity_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_block_cipher_mode_uniformity_guard.rs +// (CR-CHAT-04 - Padding block cipher mode uniformity guard, +// Wave-120 Lane B, requirement R-CHAT-4). +// +// Padded ciphertext blocks must be statistically uniform: a +// padding block encrypted under a different distribution than a +// payload block is separable by frequency analysis, ECB-style +// modes fingerprint repeated patterns, and a chi-squared probe +// reads the mode off the wire. The rules, in the original's +// order: the batch ceiling, then per block the size check, the +// zero-block probe, the entropy floor, the repeated-hash scan +// and the repeated-index scan. +// +// The original returns Result<(), BlockUniformityError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the six error variants in the original's +// declaration order - BelowMinEntropy, WrongSize, ZeroBlock, +// DuplicateHash, DuplicateIndex, TooMany - with every payload +// field: idx and the measured/min entropy for BelowMinEntropy, +// idx and the got/expected sizes for WrongSize, idx for the +// zero, hash and index rules, and got and max for the ceiling. +// +// The original's Vec block data becomes the fixed [16]u8 +// form plus its length (the bytes stay, the zero-block probe +// reads them), and the Shannon entropy over the bytes arrives as +// a number the caller computed - the lane convention for +// statistics over byte buffers, and here also a necessity: the +// t27c gen-rust backend does not yet lower @log (it emits a +// zero-arity .log() call, E0061 under rustc), while the native +// backend computes it fine. Both BTreeSet dedups become scans +// over the blocks already visited. + +// Block size in bytes. +pub const PBCU_BLOCK_SIZE: usize = 16; + +// Minimum entropy per block in bits per byte. +pub const PBCU_MIN_ENTROPY: f64 = 3.0; + +// Maximum blocks per batch. +pub const PBCU_MAX_BLOCKS: usize = 4096; + +// Hash length for dedup. +pub const PBCU_HASH_LEN: usize = 32; + +// A ciphertext block record: the block index, the data length +// and sixteen data bytes, the entropy the caller measured over +// the data, and the dedup hash. +pub struct BlockRecord { + block_index: u64, + data_len: usize, + block_data: [16]u8, + entropy: f64, + block_hash: [32]u8, +} + +// Verdict codes: Ok(()) and BlockUniformityError's variants, in declaration order. +pub enum BlockUniformityCode { + Ok, + BelowMinEntropy, + WrongSize, + ZeroBlock, + DuplicateHash, + DuplicateIndex, + TooMany, +} + +// The verdict of the block uniformity validation, with every +// error payload field. +pub struct BlockUniformityVerdict { + code: BlockUniformityCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, +} + +// Whole-form equality over the fixed thirty-two hash bytes. +pub fn block_hashes_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero block - the original's iter().all zero probe. +pub fn block_data_zero(data: [16]u8) -> bool { + for i in 0..16 { + if (data[i] != 0) { + return false; + } + } + return true; +} + +// Whether block i repeats an earlier hash - the original's +// BTreeSet insert probe, answered by a scan. +pub fn hash_seen_before(blocks: []const BlockRecord, i: usize) -> bool { + for j in 0..i { + if (block_hashes_equal(blocks[j].block_hash, blocks[i].block_hash)) { + return true; + } + } + return false; +} + +// Whether block i repeats an earlier index - the original's +// second BTreeSet, answered by the same scan. +pub fn index_seen_before(blocks: []const BlockRecord, i: usize) -> bool { + for j in 0..i { + if (blocks[j].block_index == blocks[i].block_index) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate padding block cipher mode uniformity. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, then per block the size, +// the zero probe, the entropy floor, the repeated hash and the +// repeated index. +pub fn validate_block_uniformity(blocks: []const BlockRecord) -> BlockUniformityVerdict { + if (blocks.len > PBCU_MAX_BLOCKS) { + return BlockUniformityVerdict { code: BlockUniformityCode.TooMany, idx: 0, got: blocks.len, bound: PBCU_MAX_BLOCKS, got_f: 0.0, bound_f: 0.0, }; + } + for i in 0..blocks.len { + if (blocks[i].data_len != PBCU_BLOCK_SIZE) { + return BlockUniformityVerdict { code: BlockUniformityCode.WrongSize, idx: i, got: blocks[i].data_len, bound: PBCU_BLOCK_SIZE, got_f: 0.0, bound_f: 0.0, }; + } + if (block_data_zero(blocks[i].block_data)) { + return BlockUniformityVerdict { code: BlockUniformityCode.ZeroBlock, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + if (blocks[i].entropy < PBCU_MIN_ENTROPY) { + return BlockUniformityVerdict { code: BlockUniformityCode.BelowMinEntropy, idx: i, got: 0, bound: 0, got_f: blocks[i].entropy, bound_f: PBCU_MIN_ENTROPY, }; + } + if (hash_seen_before(blocks, i)) { + return BlockUniformityVerdict { code: BlockUniformityCode.DuplicateHash, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + if (index_seen_before(blocks, i)) { + return BlockUniformityVerdict { code: BlockUniformityCode.DuplicateIndex, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + } + return BlockUniformityVerdict { code: BlockUniformityCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; +} + +// The block data of the fixtures: the original's +// high_entropy_block(seed) walks (i * 79 + seed) mod 256 over +// sixteen bytes - sixteen distinct values, a true four-bit +// entropy each - and its low_entropy_block of sixteen 0x01. +pub const HIGH_ENTROPY_42: [16]u8 = [16]u8{66, 145, 224, 47, 126, 205, 28, 107, 186, 9, 88, 167, 246, 69, 148, 227,}; +pub const HIGH_ENTROPY_55: [16]u8 = [16]u8{85, 164, 243, 66, 145, 224, 47, 126, 205, 28, 107, 186, 9, 88, 167, 246,}; +pub const HIGH_ENTROPY_99: [16]u8 = [16]u8{153, 232, 55, 134, 213, 36, 115, 194, 17, 96, 175, 254, 77, 156, 235, 58,}; +pub const LOW_ENTROPY_DATA: [16]u8 = [16]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const ZERO_DATA_16: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The dedup hashes of the fixtures, one byte repeated thirty-two +// times - the original's hash(byte) helper, entry by entry. +pub const HASH_01: [32]u8 = [32]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const HASH_02: [32]u8 = [32]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const HASH_03: [32]u8 = [32]u8{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; + +// The hash bytes of the large-batch fixture: the original writes +// the big-endian u64 of i + 1 into h[0..8], and for every value +// in 1..=256 only the last two of those eight bytes can carry a +// nonzero digit. +pub const PBCU_H6_256: [256]u8 = [256]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,}; +pub const PBCU_H7_256: [256]u8 = [256]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0,}; +pub const PBCU_FLOOD_4097: [4097]u8 = [4097]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1,}; + +// **PBCU-01** - low entropy rejected: sixteen 0x01 bytes measure +// zero bits per byte against the floor of three. +test pbcu_01_low_entropy_rejected { + const blocks: [1]BlockRecord = [1]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: LOW_ENTROPY_DATA, entropy: 0.0, block_hash: HASH_01, }, + }; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.BelowMinEntropy); + assert(v.idx == 0); + assert(v.got_f == 0.0); + assert(v.bound_f == 3.0); +} + +// **PBCU-02** - wrong size rejected: eight bytes of the sixteen +// the mode requires. +test pbcu_02_wrong_size_rejected { + const blocks: [1]BlockRecord = [1]BlockRecord{ + BlockRecord { block_index: 0, data_len: 8, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: HASH_01, }, + }; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.WrongSize); + assert(v.idx == 0); + assert(v.got == 8); + assert(v.bound == 16); +} + +// **PBCU-03** - zero block rejected: sixteen zero bytes under a +// live hash. +test pbcu_03_zero_block_rejected { + const blocks: [1]BlockRecord = [1]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: ZERO_DATA_16, entropy: 4.0, block_hash: HASH_01, }, + }; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.ZeroBlock); + assert(v.idx == 0); +} + +// **PBCU-04** - duplicate hash rejected: the second block +// repeats the first's hash under different data. +test pbcu_04_duplicate_hash_rejected { + const blocks: [2]BlockRecord = [2]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: HASH_01, }, + BlockRecord { block_index: 1, data_len: 16, block_data: HIGH_ENTROPY_55, entropy: 4.0, block_hash: HASH_01, }, + }; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.DuplicateHash); + assert(v.idx == 1); +} + +// **PBCU-05** - duplicate index rejected: the second block +// repeats the first's index under a different hash. +test pbcu_05_duplicate_index_rejected { + const blocks: [2]BlockRecord = [2]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: HASH_01, }, + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_55, entropy: 4.0, block_hash: HASH_02, }, + }; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.DuplicateIndex); + assert(v.idx == 1); +} + +// **PBCU-06** - too many rejected: 4097 blocks of a maximum +// 4096, built in a loop like the original's. The batch rule +// fires before any block is examined, so every record carries +// the same minimal block. +test pbcu_06_too_many_rejected { + var blocks: [4097]BlockRecord; + for i in 0..4097 { + var h: [32]u8; + for j in 0..32 { + h[j] = 0; + } + h[0] = PBCU_FLOOD_4097[i]; + blocks[i] = BlockRecord { block_index: @as(u64, i), data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: h, }; + } + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.TooMany); + assert(v.got == 4097); + assert(v.bound == 4096); +} + +// **PBCU-07** - the valid batch accepted: three blocks, three +// seeds, three hashes, three indices. +test pbcu_07_valid_accepted { + const blocks: [3]BlockRecord = [3]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: HASH_01, }, + BlockRecord { block_index: 1, data_len: 16, block_data: HIGH_ENTROPY_55, entropy: 4.0, block_hash: HASH_02, }, + BlockRecord { block_index: 2, data_len: 16, block_data: HIGH_ENTROPY_99, entropy: 4.0, block_hash: HASH_03, }, + }; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.Ok); +} + +// **PBCU-08** - an empty batch accepted. +test pbcu_08_empty_accepted { + const blocks: [0]BlockRecord = [0]BlockRecord{}; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.Ok); +} + +// **PBCU-09** - a single block accepted. +test pbcu_09_single_accepted { + const blocks: [1]BlockRecord = [1]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: HASH_01, }, + }; + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.Ok); +} + +// **PBCU-10** - a large batch accepted: 256 blocks, each hash +// the big-endian u64 of its index plus one. Every block carries +// the same sixteen-byte spread - for each index the original's +// (i * 79 + seed) walk yields sixteen distinct bytes and the +// same four-bit entropy, and the entropy rule reads the +// caller-computed field, never the data. +test pbcu_10_large_batch_accepted { + var blocks: [256]BlockRecord; + for i in 0..256 { + var h: [32]u8; + for j in 0..32 { + h[j] = 0; + } + h[6] = PBCU_H6_256[i]; + h[7] = PBCU_H7_256[i]; + blocks[i] = BlockRecord { block_index: @as(u64, i), data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: h, }; + } + const v = validate_block_uniformity(&blocks); + assert(v.code == BlockUniformityCode.Ok); +} + +// The four helpers have direct tests (coverage: Untested 0). +test pbcu_helpers_answer { + assert(block_hashes_equal(HASH_01, HASH_01)); + assert(!block_hashes_equal(HASH_01, HASH_02)); + assert(block_data_zero(ZERO_DATA_16)); + assert(!block_data_zero(HIGH_ENTROPY_42)); + const three: [3]BlockRecord = [3]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: HASH_01, }, + BlockRecord { block_index: 1, data_len: 16, block_data: HIGH_ENTROPY_55, entropy: 4.0, block_hash: HASH_02, }, + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_99, entropy: 4.0, block_hash: HASH_01, }, + }; + assert(!hash_seen_before(&three, 0)); + assert(!hash_seen_before(&three, 1)); + assert(hash_seen_before(&three, 2)); + assert(!index_seen_before(&three, 0)); + assert(!index_seen_before(&three, 1)); + assert(index_seen_before(&three, 2)); +} + +// Rule order is the original's: the size before the zero probe, +// the zero probe before the entropy floor, the entropy floor +// before the hash scan, the hash scan before the index scan - +// and the batch ceiling before any block. +test rule_order_matches_original { + const short_zero: [1]BlockRecord = [1]BlockRecord{ + BlockRecord { block_index: 0, data_len: 8, block_data: ZERO_DATA_16, entropy: 0.0, block_hash: HASH_01, }, + }; + const v1 = validate_block_uniformity(&short_zero); + assert(v1.code == BlockUniformityCode.WrongSize); + + const zero_low: [1]BlockRecord = [1]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: ZERO_DATA_16, entropy: 0.0, block_hash: HASH_01, }, + }; + const v2 = validate_block_uniformity(&zero_low); + assert(v2.code == BlockUniformityCode.ZeroBlock); + + const low_twin_hash: [2]BlockRecord = [2]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 0.0, block_hash: HASH_01, }, + BlockRecord { block_index: 1, data_len: 16, block_data: HIGH_ENTROPY_55, entropy: 4.0, block_hash: HASH_01, }, + }; + const v3 = validate_block_uniformity(&low_twin_hash); + assert(v3.code == BlockUniformityCode.BelowMinEntropy); + assert(v3.idx == 0); + + const twin_hash_twin_index: [2]BlockRecord = [2]BlockRecord{ + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_42, entropy: 4.0, block_hash: HASH_01, }, + BlockRecord { block_index: 0, data_len: 16, block_data: HIGH_ENTROPY_55, entropy: 4.0, block_hash: HASH_01, }, + }; + const v4 = validate_block_uniformity(&twin_hash_twin_index); + assert(v4.code == BlockUniformityCode.DuplicateHash); + assert(v4.idx == 1); + + var flooded: [4097]BlockRecord; + for i in 0..4097 { + flooded[i] = BlockRecord { block_index: 0, data_len: 16, block_data: ZERO_DATA_16, entropy: 0.0, block_hash: HASH_01, }; + } + const v5 = validate_block_uniformity(&flooded); + assert(v5.code == BlockUniformityCode.TooMany); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_class_balance_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_class_balance_guard.t27 new file mode 100644 index 0000000000..56e2e0f7be --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_class_balance_guard.t27 @@ -0,0 +1,401 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_class_balance_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_class_balance_guard.rs +// (CR-CHAT-04 - Padding ciphertext length class balance guard, +// Wave-131 Lane B, requirement R-CHAT-4). +// +// Ciphertext must spread evenly over the length classes: the +// most populated class likely holds the real messages, a sparse +// class reads as cover, and the class distribution profile +// fingerprints the user. The rules, in the original's order: the +// batch ceiling, the record floor, then per record the zero-size +// probe, the class-index ceiling and the repeated-id scan, then +// over the whole batch the chi-squared ceiling on the class +// counts. +// +// The original returns Result<(), ClassBalanceError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six error variants in the original's +// declaration order - Imbalanced, TooFew, InvalidClass, +// DuplicateRecordId, ZeroSize, TooMany - with every payload +// field: the measured and ceiling chi-squared for Imbalanced, +// got and min for TooFew, idx, got and max for InvalidClass, idx +// for the id and size rules, and got and max for the ceiling. +// +// The class counts accumulate in f64 from the start (the +// int-to-float cast the backends reject), the arithmetic +// identical for integer counts. The BTreeSet dedup becomes a +// scan over the records already visited. + +// Number of length classes. +pub const PCLB_NUM_CLASSES: usize = 4; + +// Maximum chi-squared statistic. +pub const PCLB_MAX_CHI_SQUARED: f64 = 15.0; + +// Minimum records for chi-squared validity. +pub const PCLB_MIN_RECORDS: usize = 16; + +// Maximum records per batch. +pub const PCLB_MAX_RECORDS: usize = 4096; + +// Record ID length. +pub const PCLB_RECORD_ID_LEN: usize = 32; + +// A ciphertext length class record. +pub struct ClassRecord { + record_id: [32]u8, + class_index: usize, + size: usize, +} + +// Verdict codes: Ok(()) and ClassBalanceError's variants, in declaration order. +pub enum ClassBalanceCode { + Ok, + Imbalanced, + TooFew, + InvalidClass, + DuplicateRecordId, + ZeroSize, + TooMany, +} + +// The verdict of the class balance validation, with every error +// payload field. +pub struct ClassBalanceVerdict { + code: ClassBalanceCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, +} + +// Whole-form equality over the fixed thirty-two record-id +// bytes. +pub fn record_ids_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether record i repeats an earlier id - the original's +// BTreeSet insert probe, answered by a scan. +pub fn rid_seen_before(records: []const ClassRecord, i: usize) -> bool { + for j in 0..i { + if (record_ids_equal(records[j].record_id, records[i].record_id)) { + return true; + } + } + return false; +} + +// The chi-squared distance of the observed class counts from a +// flat distribution - the original's chi_squared_flat over the +// f64 count table. A zero total answers zero, and so does a +// zero expectation. +pub fn chi_squared_flat(observed: [4]f64) -> f64 { + var total: f64 = 0.0; + for i in 0..4 { + total = total + observed[i]; + } + if (total == 0.0) { + return 0.0; + } + const expected: f64 = total / 4.0; + if (expected == 0.0) { + return 0.0; + } + var chi: f64 = 0.0; + var diff: f64 = 0.0; + for i in 0..4 { + diff = observed[i] - expected; + chi = chi + diff * diff / expected; + } + return chi; +} + +// [VERIFIED] Validate padding ciphertext length class balance. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, the record floor, then +// per record the zero size, the class ceiling and the repeated +// id, then the chi-squared ceiling over the class counts. +pub fn validate_class_balance(records: []const ClassRecord) -> ClassBalanceVerdict { + if (records.len > PCLB_MAX_RECORDS) { + return ClassBalanceVerdict { code: ClassBalanceCode.TooMany, idx: 0, got: records.len, bound: PCLB_MAX_RECORDS, got_f: 0.0, bound_f: 0.0, }; + } + if (records.len < PCLB_MIN_RECORDS) { + return ClassBalanceVerdict { code: ClassBalanceCode.TooFew, idx: 0, got: records.len, bound: PCLB_MIN_RECORDS, got_f: 0.0, bound_f: 0.0, }; + } + var counts: [4]f64 = [4]f64{0.0, 0.0, 0.0, 0.0,}; + for i in 0..records.len { + if (records[i].size == 0) { + return ClassBalanceVerdict { code: ClassBalanceCode.ZeroSize, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + if (records[i].class_index >= PCLB_NUM_CLASSES) { + return ClassBalanceVerdict { code: ClassBalanceCode.InvalidClass, idx: i, got: records[i].class_index, bound: PCLB_NUM_CLASSES - 1, got_f: 0.0, bound_f: 0.0, }; + } + if (rid_seen_before(records, i)) { + return ClassBalanceVerdict { code: ClassBalanceCode.DuplicateRecordId, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + counts[records[i].class_index] = counts[records[i].class_index] + 1.0; + } + const chi: f64 = chi_squared_flat(counts); + if (chi > PCLB_MAX_CHI_SQUARED) { + return ClassBalanceVerdict { code: ClassBalanceCode.Imbalanced, idx: 0, got: 0, bound: 0, got_f: chi, bound_f: PCLB_MAX_CHI_SQUARED, }; + } + return ClassBalanceVerdict { code: ClassBalanceCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; +} + +// The record ids and classes of the fixtures, and the class +// sizes of the balanced batch - the original's helpers, entry by +// entry. +pub const PCLB_SIZES: [4]usize = [4]usize{512, 768, 1024, 1280,}; +pub const PCLB_ID_BYTES_16: [16]u8 = [16]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,}; +pub const PCLB_ID_BYTES_15: [15]u8 = [15]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,}; +pub const PCLB_ID_BYTES_18: [18]u8 = [18]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,}; +pub const PCLB_ID_BYTES_200: [200]u8 = [200]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200,}; +pub const PCLB_CLASSES_18: [18]usize = [18]usize{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,}; +// The class walk of the exact-boundary fixture: two, eight and +// eleven records over the last three classes - a chi-squared of +// exactly fifteen, none in the first class. +pub const PCLB_CLASSES_21: [21]usize = [21]usize{1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; +pub const RID_01: [32]u8 = [32]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const RID_FF: [32]u8 = [32]u8{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,}; +pub const RID_FIRST: [32]u8 = [32]u8{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; +pub const ZERO_RID: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +// The two id bytes of the too-many fixture: the original writes +// the big-endian u64 of i + 1 into id[0..8], and for every value +// in 1..=4097 only the last two of those eight bytes can carry a +// nonzero digit. +pub const PCLB_ID6_4097: [4097]u8 = [4097]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16,}; +pub const PCLB_ID7_4097: [4097]u8 = [4097]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1,}; + +// **PCLB-01** - imbalanced rejected: twenty records in the first +// class - chi-squared sixty over the ceiling of fifteen. +test pclb_01_imbalanced_rejected { + var records: [20]ClassRecord; + for i in 0..20 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_200[i]; + records[i] = ClassRecord { record_id: id, class_index: 0, size: 512, }; + } + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.Imbalanced); + assert(v.got_f == 60.0); + assert(v.bound_f == 15.0); +} + +// **PCLB-02** - too few rejected: fifteen records of a required +// sixteen. +test pclb_02_too_few_rejected { + var records: [15]ClassRecord; + for i in 0..15 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_15[i]; + records[i] = ClassRecord { record_id: id, class_index: i % 4, size: 512, }; + } + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.TooFew); + assert(v.got == 15); + assert(v.bound == 16); +} + +// **PCLB-03** - invalid class rejected: the balanced batch plus +// one record claiming class four of a zero-indexed maximum +// three. +test pclb_03_invalid_class_rejected { + var records: [17]ClassRecord; + for i in 0..16 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_16[i]; + records[i] = ClassRecord { record_id: id, class_index: i / 4, size: PCLB_SIZES[i / 4], }; + } + records[16] = ClassRecord { record_id: RID_FF, class_index: 4, size: 512, }; + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.InvalidClass); + assert(v.idx == 16); + assert(v.got == 4); + assert(v.bound == 3); +} + +// **PCLB-04** - duplicate record id rejected: the balanced batch +// plus one record repeating the first id. +test pclb_04_duplicate_rejected { + var records: [17]ClassRecord; + for i in 0..16 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_16[i]; + records[i] = ClassRecord { record_id: id, class_index: i / 4, size: PCLB_SIZES[i / 4], }; + } + records[16] = ClassRecord { record_id: RID_FIRST, class_index: 0, size: 512, }; + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.DuplicateRecordId); + assert(v.idx == 16); +} + +// **PCLB-05** - zero size rejected under a live id. +test pclb_05_zero_size_rejected { + var records: [17]ClassRecord; + for i in 0..16 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_16[i]; + records[i] = ClassRecord { record_id: id, class_index: i / 4, size: PCLB_SIZES[i / 4], }; + } + records[16] = ClassRecord { record_id: RID_FF, class_index: 0, size: 0, }; + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.ZeroSize); + assert(v.idx == 16); +} + +// **PCLB-06** - too many rejected: 4097 records of a maximum +// 4096, built in a loop like the original's, each id carrying +// the big-endian u64 of its index plus one. The batch rule fires +// before any record is examined. +test pclb_06_too_many_rejected { + var records: [4097]ClassRecord; + for i in 0..4097 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[6] = PCLB_ID6_4097[i]; + id[7] = PCLB_ID7_4097[i]; + records[i] = ClassRecord { record_id: id, class_index: i % 4, size: 512, }; + } + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.TooMany); + assert(v.got == 4097); + assert(v.bound == 4096); +} + +// **PCLB-07** - the balanced batch accepted: four records per +// class, the sizes walking the class ladder. +test pclb_07_balanced_accepted { + var records: [16]ClassRecord; + for i in 0..16 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_16[i]; + records[i] = ClassRecord { record_id: id, class_index: i / 4, size: PCLB_SIZES[i / 4], }; + } + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.Ok); +} + +// **PCLB-08** - exactly the minimum accepted: sixteen records +// cycling the classes. +test pclb_08_exact_minimum_accepted { + var records: [16]ClassRecord; + for i in 0..16 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_16[i]; + records[i] = ClassRecord { record_id: id, class_index: i % 4, size: 512, }; + } + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.Ok); +} + +// **PCLB-09** - slight imbalance accepted: six records in the +// first class, four elsewhere - chi-squared of two thirds, well +// under the ceiling. +test pclb_09_slight_imbalance_accepted { + var records: [18]ClassRecord; + for i in 0..18 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_18[i]; + records[i] = ClassRecord { record_id: id, class_index: PCLB_CLASSES_18[i], size: 512, }; + } + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.Ok); +} + +// **PCLB-10** - a large balanced batch accepted: two hundred +// records cycling the classes. +test pclb_10_large_batch_accepted { + var records: [200]ClassRecord; + for i in 0..200 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_200[i]; + records[i] = ClassRecord { record_id: id, class_index: i % 4, size: 512, }; + } + const v = validate_class_balance(&records); + assert(v.code == ClassBalanceCode.Ok); +} + +// The three helpers have direct tests (coverage: Untested 0). +test pclb_helpers_answer { + assert(record_ids_equal(RID_01, RID_01)); + assert(!record_ids_equal(RID_01, RID_FF)); + const three: [3]ClassRecord = [3]ClassRecord{ + ClassRecord { record_id: RID_01, class_index: 0, size: 512, }, + ClassRecord { record_id: RID_FF, class_index: 1, size: 512, }, + ClassRecord { record_id: RID_01, class_index: 2, size: 512, }, + }; + assert(!rid_seen_before(&three, 0)); + assert(!rid_seen_before(&three, 1)); + assert(rid_seen_before(&three, 2)); + const flat: [4]f64 = [4]f64{4.0, 4.0, 4.0, 4.0,}; + assert(chi_squared_flat(flat) == 0.0); + const skew: [4]f64 = [4]f64{20.0, 0.0, 0.0, 0.0,}; + assert(chi_squared_flat(skew) == 60.0); + const zeros: [4]f64 = [4]f64{0.0, 0.0, 0.0, 0.0,}; + assert(chi_squared_flat(zeros) == 0.0); +} + +// Rule order is the original's: the record floor before any +// record, the zero size before the class ceiling, the class +// ceiling before the id scan, the id scan before the batch +// chi-squared - and the chi-squared ceiling admitting its exact +// boundary of fifteen. +test rule_order_matches_original { + var short_zero: [15]ClassRecord; + for i in 0..15 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_15[i]; + short_zero[i] = ClassRecord { record_id: id, class_index: 4, size: 0, }; + } + const v1 = validate_class_balance(&short_zero); + assert(v1.code == ClassBalanceCode.TooFew); + + var zero_invalid: [16]ClassRecord; + for i in 0..16 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_16[i]; + zero_invalid[i] = ClassRecord { record_id: id, class_index: 4, size: 0, }; + } + const v2 = validate_class_balance(&zero_invalid); + assert(v2.code == ClassBalanceCode.ZeroSize); + assert(v2.idx == 0); + + var invalid_dup: [16]ClassRecord; + for i in 0..15 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_15[i]; + invalid_dup[i] = ClassRecord { record_id: id, class_index: i % 4, size: 512, }; + } + invalid_dup[15] = ClassRecord { record_id: RID_01, class_index: 4, size: 512, }; + const v3 = validate_class_balance(&invalid_dup); + assert(v3.code == ClassBalanceCode.InvalidClass); + assert(v3.idx == 15); + + var dup_imbalanced: [16]ClassRecord; + for i in 0..15 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_15[i]; + dup_imbalanced[i] = ClassRecord { record_id: id, class_index: 0, size: 512, }; + } + dup_imbalanced[15] = ClassRecord { record_id: RID_FIRST, class_index: 0, size: 512, }; + const v4 = validate_class_balance(&dup_imbalanced); + assert(v4.code == ClassBalanceCode.DuplicateRecordId); + assert(v4.idx == 15); + + var boundary: [21]ClassRecord; + for i in 0..21 { + var id: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PCLB_ID_BYTES_200[i]; + boundary[i] = ClassRecord { record_id: id, class_index: PCLB_CLASSES_21[i], size: 512, }; + } + const v5 = validate_class_balance(&boundary); + assert(v5.code == ClassBalanceCode.Ok); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_uniqueness_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_uniqueness_guard.t27 new file mode 100644 index 0000000000..54c1009bb9 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_uniqueness_guard.t27 @@ -0,0 +1,308 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_derivation_uniqueness_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_key_derivation_uniqueness_guard.rs +// (CR-CHAT-04 - Padding key derivation uniqueness guard, Wave-85 +// Lane A, requirement R-CHAT-4). +// +// Every padding operation must draw a fresh key off the ratchet: +// a reused key XOR-correlates two messages, groups traffic by +// sender through identical padding patterns, and makes the +// padding strippable and the payload sizes recoverable. The +// rules, in the original's order: the batch ceiling, then per +// derivation the empty-label probe, the key-length check, the +// zero-key probe, the strictly-increasing index and the +// repeated-key scan. +// +// The original returns Result<(), PadKeyError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six error variants in the original's +// declaration order - DuplicateKey, IndexNotIncreasing, +// WrongKeyLen, TooManyDerivations, EmptyLabel, ZeroKey - with +// every payload field: the offending index for +// IndexNotIncreasing and ZeroKey, the expected and got lengths +// for WrongKeyLen, and the batch got and max for the ceiling. +// +// The original's Vec key becomes the fixed [32]u8 form plus +// its length (the bytes stay; the zero probe and the dedup read +// them), and the String label becomes its length - the only +// property any rule reads. The BTreeSet dedup becomes a scan +// over the derivations already visited; the boundary fixture's +// little-endian index bytes come out of the identity byte +// table, no narrowing anywhere. + +// Expected padding key length. +pub const PKDU_KEY_LEN: usize = 32; + +// Maximum derivations per session. +pub const PKDU_MAX_DERIVATIONS: usize = 65536; + +// The identity byte table: slot n carries byte n. Indexing it +// with a value 0..255 is the cast the backends lower. +pub const PKDU_BYTE_TABLE: [256]u8 = [256]u8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,}; + +// A padding key derivation record: the index, the key material +// with its length, and the label's length. +pub struct PadKeyDerivation { + index: u64, + key: [32]u8, + key_len: usize, + label_len: usize, +} + +// Verdict codes: Ok(()) and PadKeyError's variants, in declaration order. +pub enum PadKeyCode { + Ok, + DuplicateKey, + IndexNotIncreasing, + WrongKeyLen, + TooManyDerivations, + EmptyLabel, + ZeroKey, +} + +// The verdict of the pad key validation, with every error payload +// field. +pub struct PadKeyVerdict { + code: PadKeyCode, + got: usize, + bound: usize, + got64: u64, +} + +// Whole-form equality over the fixed thirty-two key bytes. +pub fn keys_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero key - the original's iter().all zero probe. +pub fn key_zero(k: [32]u8) -> bool { + for i in 0..32 { + if (k[i] != 0) { + return false; + } + } + return true; +} + +// Whether derivation i repeats an earlier key - the original's +// BTreeSet insert probe, answered by a scan. +pub fn key_seen_before(derivations: []const PadKeyDerivation, i: usize) -> bool { + for j in 0..i { + if (keys_equal(derivations[j].key, derivations[i].key)) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate padding key derivation uniqueness. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, then per derivation the +// empty label, the key length, the zero key, the strictly +// increasing index and the repeated key. +pub fn validate_pad_key_derivations(derivations: []const PadKeyDerivation) -> PadKeyVerdict { + if (derivations.len > PKDU_MAX_DERIVATIONS) { + return PadKeyVerdict { code: PadKeyCode.TooManyDerivations, got: derivations.len, bound: PKDU_MAX_DERIVATIONS, got64: 0, }; + } + for i in 0..derivations.len { + if (derivations[i].label_len == 0) { + return PadKeyVerdict { code: PadKeyCode.EmptyLabel, got: 0, bound: 0, got64: 0, }; + } + if (derivations[i].key_len != PKDU_KEY_LEN) { + return PadKeyVerdict { code: PadKeyCode.WrongKeyLen, got: derivations[i].key_len, bound: PKDU_KEY_LEN, got64: 0, }; + } + if (key_zero(derivations[i].key)) { + return PadKeyVerdict { code: PadKeyCode.ZeroKey, got: 0, bound: 0, got64: derivations[i].index, }; + } + if (i > 0) { + if (derivations[i].index <= derivations[i - 1].index) { + return PadKeyVerdict { code: PadKeyCode.IndexNotIncreasing, got: 0, bound: 0, got64: derivations[i].index, }; + } + } + if (key_seen_before(derivations, i)) { + return PadKeyVerdict { code: PadKeyCode.DuplicateKey, got: 0, bound: 0, got64: 0, }; + } + } + return PadKeyVerdict { code: PadKeyCode.Ok, got: 0, bound: 0, got64: 0, }; +} + +// The key material of the fixtures, one byte repeated thirty-two +// times - the original's key(byte) helper, entry by entry. +pub const KEY_AA: [32]u8 = [32]u8{170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,}; +pub const KEY_BB: [32]u8 = [32]u8{187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187,}; +pub const KEY_CC: [32]u8 = [32]u8{204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204,}; +pub const KEY_FF: [32]u8 = [32]u8{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,}; +pub const KEY_ZERO: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// **PKDU-01** - duplicate key rejected: the second derivation +// repeats the first's material under a fresh index. +test pkdu_01_duplicate_rejected { + const ds: [2]PadKeyDerivation = [2]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_AA, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 2, key: KEY_AA, key_len: 32, label_len: 5, }, + }; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.DuplicateKey); +} + +// **PKDU-02** - index not increasing rejected: the second +// derivation carries index one under a first of two. +test pkdu_02_index_not_increasing_rejected { + const ds: [2]PadKeyDerivation = [2]PadKeyDerivation{ + PadKeyDerivation { index: 2, key: KEY_AA, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 1, key: KEY_BB, key_len: 32, label_len: 5, }, + }; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.IndexNotIncreasing); + assert(v.got64 == 1); +} + +// **PKDU-03** - wrong key length rejected: sixteen bytes of the +// thirty-two expected. +test pkdu_03_wrong_key_len_rejected { + const ds: [1]PadKeyDerivation = [1]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_AA, key_len: 16, label_len: 5, }, + }; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.WrongKeyLen); + assert(v.got == 16); + assert(v.bound == 32); +} + +// **PKDU-04** - too many rejected: 65537 derivations of a maximum +// 65536, built in a loop like the original's. The batch rule +// fires before any derivation is examined. +test pkdu_04_too_many_rejected { + var ds: [65537]PadKeyDerivation; + for i in 0..65537 { + ds[i] = PadKeyDerivation { index: @as(u64, i), key: KEY_AA, key_len: 32, label_len: 1, }; + } + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.TooManyDerivations); + assert(v.got == 65537); + assert(v.bound == 65536); +} + +// **PKDU-05** - empty label rejected under live key material. +test pkdu_05_empty_label_rejected { + const ds: [1]PadKeyDerivation = [1]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_AA, key_len: 32, label_len: 0, }, + }; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.EmptyLabel); +} + +// **PKDU-06** - all-zero key rejected, its index carried back. +test pkdu_06_zero_key_rejected { + const ds: [1]PadKeyDerivation = [1]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_ZERO, key_len: 32, label_len: 5, }, + }; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.ZeroKey); + assert(v.got64 == 1); +} + +// **PKDU-07** - the valid derivations accepted: three indices, +// three key materials. +test pkdu_07_valid_accepted { + const ds: [3]PadKeyDerivation = [3]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_AA, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 2, key: KEY_BB, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 3, key: KEY_CC, key_len: 32, label_len: 5, }, + }; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.Ok); +} + +// **PKDU-08** - an empty batch accepted. +test pkdu_08_empty_accepted { + const ds: [0]PadKeyDerivation = [0]PadKeyDerivation{}; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.Ok); +} + +// **PKDU-09** - a single derivation accepted. +test pkdu_09_single_accepted { + const ds: [1]PadKeyDerivation = [1]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_FF, key_len: 32, label_len: 5, }, + }; + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.Ok); +} + +// **PKDU-10** - the maximum boundary accepted: 65536 derivations, +// each key the little-endian bytes of its index with a live +// ninth byte, indices walking zero upward. +test pkdu_10_max_boundary_accepted { + var ds: [65536]PadKeyDerivation; + for i in 0..65536 { + var k: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + k[0] = PKDU_BYTE_TABLE[i % 256]; + k[1] = PKDU_BYTE_TABLE[(i / 256) % 256]; + k[8] = 1; + ds[i] = PadKeyDerivation { index: @as(u64, i), key: k, key_len: 32, label_len: 1, }; + } + const v = validate_pad_key_derivations(&ds); + assert(v.code == PadKeyCode.Ok); +} + +// The three helpers have direct tests (coverage: Untested 0). +test pkdu_helpers_answer { + assert(keys_equal(KEY_AA, KEY_AA)); + assert(!keys_equal(KEY_AA, KEY_BB)); + assert(key_zero(KEY_ZERO)); + assert(!key_zero(KEY_AA)); + const three: [3]PadKeyDerivation = [3]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_AA, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 2, key: KEY_BB, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 3, key: KEY_AA, key_len: 32, label_len: 5, }, + }; + assert(!key_seen_before(&three, 0)); + assert(!key_seen_before(&three, 1)); + assert(key_seen_before(&three, 2)); +} + +// Rule order is the original's: the empty label before the key +// length, the key length before the zero probe, the zero probe +// before the index, the index before the repeated key - and the +// batch ceiling before any derivation. +test rule_order_matches_original { + const empty_short: [1]PadKeyDerivation = [1]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_ZERO, key_len: 16, label_len: 0, }, + }; + const v1 = validate_pad_key_derivations(&empty_short); + assert(v1.code == PadKeyCode.EmptyLabel); + + const short_zero: [1]PadKeyDerivation = [1]PadKeyDerivation{ + PadKeyDerivation { index: 1, key: KEY_ZERO, key_len: 16, label_len: 5, }, + }; + const v2 = validate_pad_key_derivations(&short_zero); + assert(v2.code == PadKeyCode.WrongKeyLen); + + const zero_backstep: [2]PadKeyDerivation = [2]PadKeyDerivation{ + PadKeyDerivation { index: 5, key: KEY_AA, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 3, key: KEY_ZERO, key_len: 32, label_len: 5, }, + }; + const v3 = validate_pad_key_derivations(&zero_backstep); + assert(v3.code == PadKeyCode.ZeroKey); + assert(v3.got64 == 3); + + const backstep_twin: [2]PadKeyDerivation = [2]PadKeyDerivation{ + PadKeyDerivation { index: 2, key: KEY_AA, key_len: 32, label_len: 5, }, + PadKeyDerivation { index: 1, key: KEY_AA, key_len: 32, label_len: 5, }, + }; + const v4 = validate_pad_key_derivations(&backstep_twin); + assert(v4.code == PadKeyCode.IndexNotIncreasing); + + var flooded: [65537]PadKeyDerivation; + for i in 0..65537 { + flooded[i] = PadKeyDerivation { index: 1, key: KEY_ZERO, key_len: 32, label_len: 0, }; + } + const v5 = validate_pad_key_derivations(&flooded); + assert(v5.code == PadKeyCode.TooManyDerivations); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_rotation_uniformity_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_rotation_uniformity_guard.t27 new file mode 100644 index 0000000000..a3f81b99c8 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_key_rotation_uniformity_guard.t27 @@ -0,0 +1,312 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_key_rotation_uniformity_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_key_rotation_uniformity_guard.rs +// (CR-CHAT-04 - Padding key rotation uniformity guard, Wave-101 +// Lane A, requirement R-CHAT-4). +// +// Padding keys must rotate on a uniform clock: regular rotation +// lets an observer align size-class changes with key boundaries, +// a stale key repeats the class-selection pattern across epochs, +// and irregular timing leaks load. The rules, in the original's +// order: the batch ceiling, then per rotation the zero-key +// probe, the interval floor and ceiling, the monotonic +// non-decreasing check and the repeated-key scan - equal +// intervals pass. +// +// The original returns Result<(), KeyRotationError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six error variants in the original's +// declaration order - TooFrequent, TooSlow, NotMonotonic, +// DuplicateKey, ZeroKey, TooMany - with every payload field: idx +// and the measured/min or measured/max interval for the bounds, +// idx with prev and current for the monotonic break, idx for the +// id rules, and got and max for the ceiling. +// +// The BTreeSet dedup becomes a scan over the rotations already +// visited; the intervals stay the original's u64 arithmetic. + +// Minimum rotation interval (messages). +pub const PKRU_MIN_INTERVAL: u64 = 100; + +// Maximum rotation interval (messages). +pub const PKRU_MAX_INTERVAL: u64 = 10000; + +// Maximum rotations per batch. +pub const PKRU_MAX_ROTATIONS: usize = 1024; + +// Key ID length. +pub const PKRU_KEY_ID_LEN: usize = 16; + +// A padding key rotation record. +pub struct KeyRotation { + key_id: [16]u8, + interval: u64, +} + +// Verdict codes: Ok(()) and KeyRotationError's variants, in declaration order. +pub enum KeyRotationCode { + Ok, + TooFrequent, + TooSlow, + NotMonotonic, + DuplicateKey, + ZeroKey, + TooMany, +} + +// The verdict of the key rotation validation, with every error +// payload field. +pub struct KeyRotationVerdict { + code: KeyRotationCode, + idx: usize, + got: usize, + bound: usize, + got64: u64, + bound64: u64, +} + +// Whole-form equality over the fixed sixteen key-id bytes. +pub fn key_ids_equal(a: [16]u8, b: [16]u8) -> bool { + for i in 0..16 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero key id - the original's [0u8; 16] probe. +pub fn key_id_zero(id: [16]u8) -> bool { + for i in 0..16 { + if (id[i] != 0) { + return false; + } + } + return true; +} + +// Whether rotation i repeats an earlier key - the original's +// BTreeSet insert probe, answered by a scan. +pub fn key_seen_before(rotations: []const KeyRotation, i: usize) -> bool { + for j in 0..i { + if (key_ids_equal(rotations[j].key_id, rotations[i].key_id)) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate padding key rotation uniformity. Returns +// Ok only when every rule holds, checked in the original's +// order: the batch ceiling, then per rotation the zero key, the +// interval floor and ceiling, the monotonic check and the +// repeated key. +pub fn validate_key_rotations(rotations: []const KeyRotation) -> KeyRotationVerdict { + if (rotations.len > PKRU_MAX_ROTATIONS) { + return KeyRotationVerdict { code: KeyRotationCode.TooMany, idx: 0, got: rotations.len, bound: PKRU_MAX_ROTATIONS, got64: 0, bound64: 0, }; + } + var prev: u64 = 0; + for i in 0..rotations.len { + if (key_id_zero(rotations[i].key_id)) { + return KeyRotationVerdict { code: KeyRotationCode.ZeroKey, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (rotations[i].interval < PKRU_MIN_INTERVAL) { + return KeyRotationVerdict { code: KeyRotationCode.TooFrequent, idx: i, got: 0, bound: 0, got64: rotations[i].interval, bound64: PKRU_MIN_INTERVAL, }; + } + if (rotations[i].interval > PKRU_MAX_INTERVAL) { + return KeyRotationVerdict { code: KeyRotationCode.TooSlow, idx: i, got: 0, bound: 0, got64: rotations[i].interval, bound64: PKRU_MAX_INTERVAL, }; + } + if (i > 0) { + if (rotations[i].interval < prev) { + return KeyRotationVerdict { code: KeyRotationCode.NotMonotonic, idx: i, got: 0, bound: 0, got64: rotations[i].interval, bound64: prev, }; + } + } + if (key_seen_before(rotations, i)) { + return KeyRotationVerdict { code: KeyRotationCode.DuplicateKey, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + prev = rotations[i].interval; + } + return KeyRotationVerdict { code: KeyRotationCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; +} + +// The key ids of the fixtures, one byte repeated sixteen times - +// the original's kid(byte) helper, entry by entry. +pub const KID_01: [16]u8 = [16]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const KID_02: [16]u8 = [16]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const KID_03: [16]u8 = [16]u8{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; +pub const ZERO_KID: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The leading key bytes of the too-many fixture: the original's +// (i % 254 + 1) walk over 0..=1024. +pub const PKRU_FLOOD_1025: [1025]u8 = [1025]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 1, 2, 3, 4, 5, 6, 7, 8, 9,}; + +// **PKRU-01** - too frequent rejected: ninety-nine messages of a +// floor hundred. +test pkru_01_too_frequent_rejected { + const rs: [1]KeyRotation = [1]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 99, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.TooFrequent); + assert(v.idx == 0); + assert(v.got64 == 99); + assert(v.bound64 == 100); +} + +// **PKRU-02** - too slow rejected: 10001 messages of a ceiling +// ten thousand. +test pkru_02_too_slow_rejected { + const rs: [1]KeyRotation = [1]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 10001, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.TooSlow); + assert(v.idx == 0); + assert(v.got64 == 10001); + assert(v.bound64 == 10000); +} + +// **PKRU-03** - not monotonic rejected: four hundred after five +// hundred. +test pkru_03_not_monotonic_rejected { + const rs: [2]KeyRotation = [2]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_02, interval: 400, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.NotMonotonic); + assert(v.idx == 1); + assert(v.got64 == 400); + assert(v.bound64 == 500); +} + +// **PKRU-04** - duplicate key rejected: the second rotation +// repeats the first's id. +test pkru_04_duplicate_rejected { + const rs: [2]KeyRotation = [2]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_01, interval: 600, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.DuplicateKey); + assert(v.idx == 1); +} + +// **PKRU-05** - zero key rejected under a live interval. +test pkru_05_zero_key_rejected { + const rs: [1]KeyRotation = [1]KeyRotation{ + KeyRotation { key_id: ZERO_KID, interval: 500, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.ZeroKey); + assert(v.idx == 0); +} + +// **PKRU-06** - too many rejected: 1025 rotations of a maximum +// 1024, built in a loop like the original's, each key carrying +// its walk byte and the minimum interval. The batch rule fires +// before any rotation is examined. +test pkru_06_too_many_rejected { + var rs: [1025]KeyRotation; + for i in 0..1025 { + var id: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + id[0] = PKRU_FLOOD_1025[i]; + rs[i] = KeyRotation { key_id: id, interval: 100, }; + } + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.TooMany); + assert(v.got == 1025); + assert(v.bound == 1024); +} + +// **PKRU-07** - the valid rotations accepted: five hundred, +// five hundred, six hundred - non-decreasing. +test pkru_07_valid_accepted { + const rs: [3]KeyRotation = [3]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_02, interval: 500, }, + KeyRotation { key_id: KID_03, interval: 600, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.Ok); +} + +// **PKRU-08** - an empty batch accepted. +test pkru_08_empty_accepted { + const rs: [0]KeyRotation = [0]KeyRotation{}; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.Ok); +} + +// **PKRU-09** - a single rotation at the exact floor accepted. +test pkru_09_min_interval_accepted { + const rs: [1]KeyRotation = [1]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 100, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.Ok); +} + +// **PKRU-10** - equal intervals accepted: non-decreasing means +// equal passes. +test pkru_10_equal_intervals_accepted { + const rs: [3]KeyRotation = [3]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_02, interval: 500, }, + KeyRotation { key_id: KID_03, interval: 500, }, + }; + const v = validate_key_rotations(&rs); + assert(v.code == KeyRotationCode.Ok); +} + +// The three helpers have direct tests (coverage: Untested 0). +test pkru_helpers_answer { + assert(key_ids_equal(KID_01, KID_01)); + assert(!key_ids_equal(KID_01, KID_02)); + assert(key_id_zero(ZERO_KID)); + assert(!key_id_zero(KID_01)); + const three: [3]KeyRotation = [3]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_02, interval: 500, }, + KeyRotation { key_id: KID_01, interval: 500, }, + }; + assert(!key_seen_before(&three, 0)); + assert(!key_seen_before(&three, 1)); + assert(key_seen_before(&three, 2)); +} + +// Rule order is the original's: the zero key before the interval +// floor, the floor before the monotonic check, the ceiling +// before the repeated key, the monotonic check before the +// repeated key. +test rule_order_matches_original { + const zero_fast: [1]KeyRotation = [1]KeyRotation{ + KeyRotation { key_id: ZERO_KID, interval: 50, }, + }; + const v1 = validate_key_rotations(&zero_fast); + assert(v1.code == KeyRotationCode.ZeroKey); + + const fast_backstep: [2]KeyRotation = [2]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_02, interval: 50, }, + }; + const v2 = validate_key_rotations(&fast_backstep); + assert(v2.code == KeyRotationCode.TooFrequent); + assert(v2.idx == 1); + + const slow_twin: [2]KeyRotation = [2]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_01, interval: 20000, }, + }; + const v3 = validate_key_rotations(&slow_twin); + assert(v3.code == KeyRotationCode.TooSlow); + assert(v3.idx == 1); + + const backstep_twin: [2]KeyRotation = [2]KeyRotation{ + KeyRotation { key_id: KID_01, interval: 500, }, + KeyRotation { key_id: KID_01, interval: 400, }, + }; + const v4 = validate_key_rotations(&backstep_twin); + assert(v4.code == KeyRotationCode.NotMonotonic); + assert(v4.idx == 1); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_nonce_reuse_detection_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_nonce_reuse_detection_guard.t27 new file mode 100644 index 0000000000..0d8c252937 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_nonce_reuse_detection_guard.t27 @@ -0,0 +1,334 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_nonce_reuse_detection_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_nonce_reuse_detection_guard.rs +// (CR-CHAT-04 - Padding nonce reuse detection guard, Wave-105 Lane A, +// requirement R-CHAT-4). +// +// Padding nonces must never repeat under the same key: a repeated +// nonce XORs two ciphertexts into the XOR of their plaintexts, +// enables third-ciphertext forgeries, and in some AEAD modes a +// practical key recovery. The rules, in the original's order: the +// batch ceiling, then per record the zero-key probe, the zero-nonce +// probe, the nonce-value ceiling and the repeated-pair scan. +// +// The original returns Result<(), NonceReuseError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the five error variants in the original's declaration +// order - Duplicate, ZeroNonce, ZeroKey, NonceOverflow, TooMany - +// with every payload field: idx for Duplicate, ZeroNonce, ZeroKey +// and NonceOverflow, got and max for TooMany. +// +// The original's BTreeSet of (key_id, nonce) pairs becomes a scan +// over the records already visited (conventions section 4). The +// nonce value is the big-endian u64 of the nonce's last eight +// bytes, folded byte by byte with the int-widening cast both +// backends lower - no shift-and-truncate anywhere. + +// Nonce length. +pub const PNRD_NONCE_LEN: usize = 12; + +// Key ID length. +pub const PNRD_KEY_ID_LEN: usize = 16; + +// Maximum records per batch. +pub const PNRD_MAX_RECORDS: usize = 10000; + +// Maximum nonce value (big-endian u96): two to the forty-eighth +// less one, the original's (1 << 48) - 1. +pub const PNRD_MAX_NONCE: u64 = 281474976710655; + +// A nonce usage record. +pub struct NonceRecord { + key_id: [16]u8, + nonce: [12]u8, +} + +// Verdict codes: Ok(()) and NonceReuseError's variants, in declaration order. +pub enum NonceReuseCode { + Ok, + Duplicate, + ZeroNonce, + ZeroKey, + NonceOverflow, + TooMany, +} + +// The verdict of the nonce reuse validation, with every error +// payload field. +pub struct NonceReuseVerdict { + code: NonceReuseCode, + idx: usize, + got: usize, + bound: usize, +} + +// Whole-form equality over the fixed sixteen key ID bytes. +pub fn key_ids_equal(a: [16]u8, b: [16]u8) -> bool { + for i in 0..16 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero key ID - the original's [0u8; 16] probe. +pub fn key_id_zero(k: [16]u8) -> bool { + for i in 0..16 { + if (k[i] != 0) { + return false; + } + } + return true; +} + +// Whole-form equality over the fixed twelve nonce bytes. +pub fn nonces_equal(a: [12]u8, b: [12]u8) -> bool { + for i in 0..12 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero nonce - the original's [0u8; 12] probe. +pub fn nonce_zero(n: [12]u8) -> bool { + for i in 0..12 { + if (n[i] != 0) { + return false; + } + } + return true; +} + +// The big-endian u64 of the nonce's last eight bytes - the +// original's nonce_to_u64, folded byte by byte through the +// int-widening cast. +pub fn nonce_to_u64(n: [12]u8) -> u64 { + var v: u64 = 0; + var wide: u64 = 0; + for i in 0..8 { + wide = @as(u64, n[i + 4]); + v = v * 256 + wide; + } + return v; +} + +// Whether record i repeats an earlier (key ID, nonce) pair - the +// original's BTreeSet insert probe, answered by a scan. +pub fn pair_seen_before(records: []const NonceRecord, i: usize) -> bool { + for j in 0..i { + if (key_ids_equal(records[j].key_id, records[i].key_id)) { + if (nonces_equal(records[j].nonce, records[i].nonce)) { + return true; + } + } + } + return false; +} + +// [VERIFIED] Validate padding nonce reuse. Returns Ok only when +// every rule holds, checked in the original's order: the batch +// ceiling, then per record the zero key, the zero nonce, the nonce +// value ceiling and the repeated pair. +pub fn validate_nonce_reuse(records: []const NonceRecord) -> NonceReuseVerdict { + if (records.len > PNRD_MAX_RECORDS) { + return NonceReuseVerdict { code: NonceReuseCode.TooMany, idx: 0, got: records.len, bound: PNRD_MAX_RECORDS, }; + } + for i in 0..records.len { + if (key_id_zero(records[i].key_id)) { + return NonceReuseVerdict { code: NonceReuseCode.ZeroKey, idx: i, got: 0, bound: 0, }; + } + if (nonce_zero(records[i].nonce)) { + return NonceReuseVerdict { code: NonceReuseCode.ZeroNonce, idx: i, got: 0, bound: 0, }; + } + if (nonce_to_u64(records[i].nonce) > PNRD_MAX_NONCE) { + return NonceReuseVerdict { code: NonceReuseCode.NonceOverflow, idx: i, got: 0, bound: 0, }; + } + if (pair_seen_before(records, i)) { + return NonceReuseVerdict { code: NonceReuseCode.Duplicate, idx: i, got: 0, bound: 0, }; + } + } + return NonceReuseVerdict { code: NonceReuseCode.Ok, idx: 0, got: 0, bound: 0, }; +} + +// The key IDs of the fixtures, one byte repeated sixteen times. +pub const KID_01: [16]u8 = [16]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const KID_02: [16]u8 = [16]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const ZERO_KID: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The nonces of the fixtures: twelve bytes, the last eight the +// big-endian form of the value - the original's nonce(val) +// helper, entry by entry. +pub const NONCE_1: [12]u8 = [12]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,}; +pub const NONCE_2: [12]u8 = [12]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,}; +pub const NONCE_42: [12]u8 = [12]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42,}; +pub const NONCE_MAX: [12]u8 = [12]u8{0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255,}; +pub const NONCE_OVER: [12]u8 = [12]u8{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,}; +pub const ZERO_NONCE: [12]u8 = [12]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// **PNRD-01** - duplicate rejected: the second record repeats the +// first's key and nonce. +test pnrd_01_duplicate_rejected { + const records: [2]NonceRecord = [2]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_42, }, + NonceRecord { key_id: KID_01, nonce: NONCE_42, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.Duplicate); + assert(v.idx == 1); +} + +// **PNRD-02** - zero nonce rejected under a live key. +test pnrd_02_zero_nonce_rejected { + const records: [1]NonceRecord = [1]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: ZERO_NONCE, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.ZeroNonce); + assert(v.idx == 0); +} + +// **PNRD-03** - zero key rejected under a live nonce. +test pnrd_03_zero_key_rejected { + const records: [1]NonceRecord = [1]NonceRecord{ + NonceRecord { key_id: ZERO_KID, nonce: NONCE_1, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.ZeroKey); + assert(v.idx == 0); +} + +// **PNRD-04** - nonce overflow rejected: the nonce carries two to +// the forty-eighth, one over the maximum. +test pnrd_04_nonce_overflow_rejected { + const records: [1]NonceRecord = [1]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_OVER, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.NonceOverflow); + assert(v.idx == 0); +} + +// **PNRD-05** - too many rejected: 10001 records of a maximum +// 10000, built in a loop like the original's. The batch rule fires +// before any record is examined, so every record carries the same +// minimal pair. +test pnrd_05_too_many_rejected { + var records: [10001]NonceRecord; + for i in 0..10001 { + records[i] = NonceRecord { key_id: KID_01, nonce: NONCE_1, }; + } + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.TooMany); + assert(v.got == 10001); + assert(v.bound == 10000); +} + +// **PNRD-06** - the same nonce under a different key accepted. +test pnrd_06_same_nonce_diff_key_accepted { + const records: [2]NonceRecord = [2]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_42, }, + NonceRecord { key_id: KID_02, nonce: NONCE_42, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.Ok); +} + +// **PNRD-07** - the valid batch accepted: two nonces under the +// first key, the first nonce again under the second. +test pnrd_07_valid_accepted { + const records: [3]NonceRecord = [3]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_1, }, + NonceRecord { key_id: KID_01, nonce: NONCE_2, }, + NonceRecord { key_id: KID_02, nonce: NONCE_1, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.Ok); +} + +// **PNRD-08** - an empty batch accepted. +test pnrd_08_empty_accepted { + const records: [0]NonceRecord = [0]NonceRecord{}; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.Ok); +} + +// **PNRD-09** - a single record accepted. +test pnrd_09_single_accepted { + const records: [1]NonceRecord = [1]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_1, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.Ok); +} + +// **PNRD-10** - the boundary nonce accepted: the maximum value +// itself, one byte over nothing. +test pnrd_10_boundary_nonce_accepted { + const records: [1]NonceRecord = [1]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_MAX, }, + }; + const v = validate_nonce_reuse(&records); + assert(v.code == NonceReuseCode.Ok); +} + +// The six helpers have direct tests (coverage: Untested 0). +test byte_form_helpers_answer { + assert(key_ids_equal(KID_01, KID_01)); + assert(!key_ids_equal(KID_01, KID_02)); + assert(key_id_zero(ZERO_KID)); + assert(!key_id_zero(KID_01)); + assert(nonces_equal(NONCE_1, NONCE_1)); + assert(!nonces_equal(NONCE_1, NONCE_2)); + assert(nonce_zero(ZERO_NONCE)); + assert(!nonce_zero(NONCE_1)); +} + +test nonce_to_u64_answers { + assert(nonce_to_u64(NONCE_1) == 1); + assert(nonce_to_u64(NONCE_42) == 42); + assert(nonce_to_u64(NONCE_MAX) == 281474976710655); + assert(nonce_to_u64(NONCE_OVER) == 281474976710656); + assert(nonce_to_u64(ZERO_NONCE) == 0); +} + +test pair_seen_before_answers { + const records: [3]NonceRecord = [3]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_1, }, + NonceRecord { key_id: KID_01, nonce: NONCE_2, }, + NonceRecord { key_id: KID_01, nonce: NONCE_1, }, + }; + assert(!pair_seen_before(&records, 0)); + assert(!pair_seen_before(&records, 1)); + assert(pair_seen_before(&records, 2)); +} + +// Rule order is the original's: the zero key before the zero nonce, +// the zero nonce before the repeated pair, and the nonce ceiling +// before the repeated pair. +test rule_order_matches_original { + const both_zero: [1]NonceRecord = [1]NonceRecord{ + NonceRecord { key_id: ZERO_KID, nonce: ZERO_NONCE, }, + }; + const v1 = validate_nonce_reuse(&both_zero); + assert(v1.code == NonceReuseCode.ZeroKey); + assert(v1.idx == 0); + + const zero_twice: [2]NonceRecord = [2]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: ZERO_NONCE, }, + NonceRecord { key_id: KID_01, nonce: ZERO_NONCE, }, + }; + const v2 = validate_nonce_reuse(&zero_twice); + assert(v2.code == NonceReuseCode.ZeroNonce); + assert(v2.idx == 0); + + const over_twice: [2]NonceRecord = [2]NonceRecord{ + NonceRecord { key_id: KID_01, nonce: NONCE_OVER, }, + NonceRecord { key_id: KID_01, nonce: NONCE_OVER, }, + }; + const v3 = validate_nonce_reuse(&over_twice); + assert(v3.code == NonceReuseCode.NonceOverflow); + assert(v3.idx == 0); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_chosen_ct.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_chosen_ct.t27 new file mode 100644 index 0000000000..021b3f1491 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_chosen_ct.t27 @@ -0,0 +1,372 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_chosen_ct; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_chosen_ct.rs +// (CR-CHAT-04 - Chosen-ciphertext padding-oracle guard, Wave-25 +// Lane A, requirement R-CHAT-4). +// +// An active adversary submits chosen ciphertexts and reads back +// which error path fired. A Vaudenay-style oracle leaks plaintext +// a byte at a time when bad padding, bad framing and short buffers +// answer differently, so every reject must collapse into the +// single opaque error class this module declares; the length tag +// forgery, the class-edge collision and the multi-class span all +// land on DeclaredLengthOverflow or NotACanonicalClass, never on a +// class-specific tag. The tail bytes are never read - mutating one +// deep in the zero region cannot flip a verdict - and the guard +// reasons over the layout only: a zero declared length with a +// non-zero tail still accepts. +// +// The original returns Result and +// threads a mutable VerdictLedger through every call: sixteen +// consecutive rejects lock the session, an accept resets the +// streak. t27 functions are pure, so the ledger passes by value +// and the verdict carries the state the call leaves behind - +// the reject count and the session id - exactly the original's +// &mut threading in the fixed form. The verdict codes keep the +// original's declaration order: Ok first, then +// NotACanonicalClass, BufferTooShort, DeclaredLengthOverflow and +// ProbeBudgetExceeded. +// +// The class ladder is the original's crate::CLASSES, and the +// declared length is the big-endian u32 of the buffer's first +// four bytes, folded byte by byte through the int-widening cast - +// no shift-and-truncate anywhere. The saturating increment of the +// reject streak becomes a helper with an explicit ceiling guard. + +// Maximum consecutive rejects from one probe session before the +// oracle hard-fails. +pub const PROBE_BUDGET: u32 = 16; + +// The four canonical padding classes, ascending - the original's +// crate::CLASSES entry by entry. +pub const CCT_CLASSES: [4]usize = [4]usize{256, 1024, 4096, 16384,}; + +// The probe session id of the fixtures, one byte repeated +// thirty-two times - the original's s(). +pub const SESSION_32: [32]u8 = [32]u8{170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,}; + +// A probe-session ledger: the opaque session id and the count of +// consecutive rejects. +pub struct VerdictLedger { + session: [32]u8, + rejects: u32, +} + +// Verdict codes: Ok(usize) and PaddingOracleCtError's variants, in declaration order. +pub enum PaddingOracleCtCode { + Ok, + NotACanonicalClass, + BufferTooShort, + DeclaredLengthOverflow, + ProbeBudgetExceeded, +} + +// The verdict of one probe: the code, the plaintext length on +// accept, and the ledger state the call leaves behind. +pub struct ProbeVerdict { + code: PaddingOracleCtCode, + plaintext_len: usize, + rejects: u32, + session: [32]u8, +} + +// Whether the length is one of the four canonical classes - the +// original's CLASSES.contains, answered by a scan. +pub fn is_class(len: usize) -> bool { + for c in CCT_CLASSES { + if (c == len) { + return true; + } + } + return false; +} + +// The big-endian u32 of the buffer's first four bytes - the +// original's u32::from_be_bytes, folded byte by byte through the +// int-widening cast. +pub fn read_be32(b: []const u8) -> u32 { + var v: u32 = 0; + var wide: u32 = 0; + for i in 0..4 { + wide = @as(u32, b[i]); + v = v * 256 + wide; + } + return v; +} + +// The reject streak after one more reject - the original's +// saturating_add(1), ceiling guard explicit. +pub fn next_rejects(rejects: u32) -> u32 { + var bumped: u32 = rejects; + if (bumped < 4294967295) { + bumped = bumped + 1; + } + return bumped; +} + +// Whole-form equality over the fixed thirty-two session bytes. +pub fn sessions_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// [VERIFIED] Verify a chosen-ciphertext probe against the +// padding-oracle guard. Returns Ok with the plaintext length only +// when every rule holds, checked in the original's order: the +// session lock first - which never bumps the streak - then the +// buffer floor, the canonical-class scan and the declared-length +// ceiling, each reject bumping the streak, and an accept resetting +// it to zero. +pub fn verify_probe(buf: []const u8, ledger: VerdictLedger) -> ProbeVerdict { + if (ledger.rejects >= PROBE_BUDGET) { + return ProbeVerdict { code: PaddingOracleCtCode.ProbeBudgetExceeded, plaintext_len: 0, rejects: ledger.rejects, session: ledger.session, }; + } + if (buf.len < 4) { + return ProbeVerdict { code: PaddingOracleCtCode.BufferTooShort, plaintext_len: 0, rejects: next_rejects(ledger.rejects), session: ledger.session, }; + } + if (!is_class(buf.len)) { + return ProbeVerdict { code: PaddingOracleCtCode.NotACanonicalClass, plaintext_len: 0, rejects: next_rejects(ledger.rejects), session: ledger.session, }; + } + const len32: u32 = read_be32(buf); + const len: usize = @as(usize, len32); + if (len + 4 > buf.len) { + return ProbeVerdict { code: PaddingOracleCtCode.DeclaredLengthOverflow, plaintext_len: 0, rejects: next_rejects(ledger.rejects), session: ledger.session, }; + } + return ProbeVerdict { code: PaddingOracleCtCode.Ok, plaintext_len: len, rejects: 0, session: ledger.session, }; +} + +// **CCT-01** - a well-formed probe accepted, the streak reset: the +// ledger enters with five rejects and leaves clean. +test cct01_valid_probe_accepted_and_counter_resets { + var buf: [256]u8; + for j in 0..256 { + buf[j] = 0; + } + buf[3] = 10; + const led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 5, }; + const r = verify_probe(&buf, led); + assert(r.code == PaddingOracleCtCode.Ok); + assert(r.plaintext_len == 10); + assert(r.rejects == 0); +} + +// **CCT-02** - the length-tag forge: a 256-buffer declaring 253 +// lands on the single opaque overflow tag, never a class-specific +// one. +test cct02_length_tag_forge_uniform_error { + var buf: [256]u8; + for j in 0..256 { + buf[j] = 0; + } + buf[3] = 253; + const led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const r = verify_probe(&buf, led); + assert(r.code == PaddingOracleCtCode.DeclaredLengthOverflow); + assert(r.rejects == 1); +} + +// **CCT-03** - the tail-byte probe: a byte deep in the zero tail +// flipped cannot flip the verdict - both probes answer Ok with +// the same plaintext length. +test cct03_tail_byte_mutation_no_verdict_flip { + var a: [256]u8; + for j in 0..256 { + a[j] = 0; + } + a[3] = 4; + a[200] = 0; + var b: [256]u8; + for j in 0..256 { + b[j] = 0; + } + b[3] = 4; + b[200] = 255; + const led_a: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const led_b: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const ra = verify_probe(&a, led_a); + const rb = verify_probe(&b, led_b); + assert(ra.code == rb.code); + assert(ra.plaintext_len == rb.plaintext_len); + assert(ra.code == PaddingOracleCtCode.Ok); + assert(ra.plaintext_len == 4); +} + +// **CCT-04** - the class-edge collision: 255 and 257 both reject +// with the same NotACanonicalClass tag. +test cct04_class_edge_collision_same_error { + var lo: [255]u8; + for j in 0..255 { + lo[j] = 0; + } + var hi: [257]u8; + for j in 0..257 { + hi[j] = 0; + } + const led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const r1 = verify_probe(&lo, led); + const r2 = verify_probe(&hi, led); + assert(r1.code == PaddingOracleCtCode.NotACanonicalClass); + assert(r2.code == PaddingOracleCtCode.NotACanonicalClass); +} + +// **CCT-05** - the multi-class span: a 1024-buffer claiming 4000 +// lands on the opaque overflow tag, not a class-confusion one. +test cct05_multi_class_span_rejected_as_overflow { + var buf: [1024]u8; + for j in 0..1024 { + buf[j] = 0; + } + buf[2] = 15; + buf[3] = 160; + const led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const r = verify_probe(&buf, led); + assert(r.code == PaddingOracleCtCode.DeclaredLengthOverflow); +} + +// **CCT-06** - the zero-length forgery: a 256-buffer declaring +// zero with a non-zero tail accepts - layout-only reasoning, the +// tail is never inspected. +test cct06_zero_length_nonzero_tail_accepted { + var buf: [256]u8; + for j in 0..256 { + buf[j] = 0; + } + for j in 4..256 { + buf[j] = 205; + } + const led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const r = verify_probe(&buf, led); + assert(r.code == PaddingOracleCtCode.Ok); + assert(r.plaintext_len == 0); +} + +// **CCT-07** - a three-byte buffer collapses to BufferTooShort +// without inspecting any payload. +test cct07_short_buffer_uniform_error { + var short: [3]u8 = [3]u8{0, 0, 0,}; + const led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const r = verify_probe(&short, led); + assert(r.code == PaddingOracleCtCode.BufferTooShort); + assert(r.rejects == 1); +} + +// **CCT-08** - the probe budget: sixteen consecutive rejects lock +// the session; the seventeenth call answers ProbeBudgetExceeded +// even for an otherwise-valid buffer, and the lock never bumps +// the streak further. +test cct08_probe_budget_locks_session { + var bad: [200]u8; + for j in 0..200 { + bad[j] = 0; + } + var led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + var r: ProbeVerdict = ProbeVerdict { code: PaddingOracleCtCode.Ok, plaintext_len: 0, rejects: 0, session: SESSION_32, }; + for _ in 0..16 { + r = verify_probe(&bad, led); + led = VerdictLedger { session: r.session, rejects: r.rejects }; + } + assert(r.code == PaddingOracleCtCode.NotACanonicalClass); + assert(led.rejects == 16); + var good: [256]u8; + for j in 0..256 { + good[j] = 0; + } + good[3] = 4; + const final = verify_probe(&good, led); + assert(final.code == PaddingOracleCtCode.ProbeBudgetExceeded); + assert(final.rejects == 16); +} + +// **CCT-09** - an accept resets the streak: fifteen rejects count +// up, one valid frame zeroes the count. +test cct09_accept_resets_streak { + var bad: [200]u8; + for j in 0..200 { + bad[j] = 0; + } + var led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + var r: ProbeVerdict = ProbeVerdict { code: PaddingOracleCtCode.Ok, plaintext_len: 0, rejects: 0, session: SESSION_32, }; + for _ in 0..15 { + r = verify_probe(&bad, led); + led = VerdictLedger { session: r.session, rejects: r.rejects }; + } + assert(led.rejects == 15); + var good: [256]u8; + for j in 0..256 { + good[j] = 0; + } + good[3] = 4; + const after = verify_probe(&good, led); + assert(after.code == PaddingOracleCtCode.Ok); + assert(after.plaintext_len == 4); + assert(after.rejects == 0); +} + +// **CCT-10** - green: the budget constant is public and a fresh +// ledger carries the session and a zero streak. +test cct10_green_module_exports { + assert(PROBE_BUDGET == 16); + const led: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + assert(sessions_equal(led.session, SESSION_32)); + assert(led.rejects == 0); +} + +// The four helpers have direct tests (coverage: Untested 0). +test helpers_answer { + assert(is_class(256)); + assert(is_class(1024)); + assert(is_class(4096)); + assert(is_class(16384)); + assert(!is_class(200)); + assert(!is_class(255)); + assert(!is_class(257)); + assert(!is_class(0)); + + var ten: [4]u8 = [4]u8{0, 0, 0, 10,}; + assert(read_be32(&ten) == 10); + var span_len: [4]u8 = [4]u8{0, 0, 15, 160,}; + assert(read_be32(&span_len) == 4000); + var zero: [4]u8 = [4]u8{0, 0, 0, 0,}; + assert(read_be32(&zero) == 0); + + assert(next_rejects(0) == 1); + assert(next_rejects(15) == 16); + assert(next_rejects(4294967295) == 4294967295); + + assert(sessions_equal(SESSION_32, SESSION_32)); + var alt: [32]u8; + for j in 0..32 { + alt[j] = 170; + } + alt[0] = 171; + assert(!sessions_equal(alt, SESSION_32)); +} + +// Rule order is the original's: the session lock before anything - +// it never bumps the streak - the buffer floor before the class +// scan, and the class scan before the declared-length ceiling. +test rule_order_matches_original { + const locked: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 16, }; + var short: [3]u8 = [3]u8{0, 0, 0,}; + const v1 = verify_probe(&short, locked); + assert(v1.code == PaddingOracleCtCode.ProbeBudgetExceeded); + assert(v1.rejects == 16); + + const fresh: VerdictLedger = VerdictLedger { session: SESSION_32, rejects: 0, }; + const v2 = verify_probe(&short, fresh); + assert(v2.code == PaddingOracleCtCode.BufferTooShort); + assert(v2.rejects == 1); + + var span: [200]u8; + for j in 0..200 { + span[j] = 0; + } + span[2] = 117; + span[3] = 48; + const v3 = verify_probe(&span, fresh); + assert(v3.code == PaddingOracleCtCode.NotACanonicalClass); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 new file mode 100644 index 0000000000..f8b1280fc8 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 @@ -0,0 +1,305 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.rs +// (CR-CHAT-04 - Padding payload entropy minimum guard, Wave-116 +// Lane B, requirement R-CHAT-4). +// +// Padded payloads must carry minimum entropy: all-zero padding or +// low-entropy runs are distinguishable from real ciphertext by a +// chi-squared probe, fingerprint the device by their pattern, and +// open a compression side channel. The rules, in the original's +// order: the batch ceiling, then per record the zero-length +// probe, the length floor and ceiling, the entropy floor and the +// repeated-hash scan. +// +// The original returns Result<(), PayloadEntropyError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six error variants in the original's declaration +// order - BelowMin, TooShort, TooLong, ZeroLength, DuplicateHash, +// TooMany - with every payload field: idx and the measured/min +// entropy for BelowMin, idx and the got/min or got/max lengths +// for the length rules, idx for the zero and duplicate rules, and +// got and max for the batch ceiling. +// +// The original's Vec payload becomes its length in the record, +// and the Shannon entropy over the bytes arrives as a number the +// caller computed - the fill-byte-rainness lane convention for +// statistics measured over byte buffers (no payload byte reaches +// the spec; the hash stays the fixed [32]u8 form). The original's +// BTreeSet dedup becomes a scan over the records already visited. + +// Minimum entropy in bits per byte. +pub const PPEM_MIN_ENTROPY: f64 = 3.0; + +// Minimum payload length. +pub const PPEM_MIN_LEN: usize = 32; + +// Maximum payload length. +pub const PPEM_MAX_LEN: usize = 16384; + +// Maximum payloads per batch. +pub const PPEM_MAX_PAYLOADS: usize = 1024; + +// Hash length for dedup. +pub const PPEM_HASH_LEN: usize = 32; + +// A padded payload record: the payload's length, the entropy the +// caller measured over its bytes, and its dedup hash. +pub struct PayloadRecord { + payload_len: usize, + entropy: f64, + payload_hash: [32]u8, +} + +// Verdict codes: Ok(()) and PayloadEntropyError's variants, in declaration order. +pub enum PayloadEntropyCode { + Ok, + BelowMin, + TooShort, + TooLong, + ZeroLength, + DuplicateHash, + TooMany, +} + +// The verdict of the payload entropy validation, with every error +// payload field. +pub struct PayloadEntropyVerdict { + code: PayloadEntropyCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, +} + +// Whole-form equality over the fixed thirty-two hash bytes. +pub fn hashes_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether record i repeats an earlier hash - the original's +// BTreeSet insert probe, answered by a scan. +pub fn hash_seen_before(records: []const PayloadRecord, i: usize) -> bool { + for j in 0..i { + if (hashes_equal(records[j].payload_hash, records[i].payload_hash)) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate padding payload entropy minimum. Returns Ok +// only when every rule holds, checked in the original's order: +// the batch ceiling, then per record the zero-length probe, the +// length floor and ceiling, the entropy floor and the repeated +// hash. +pub fn validate_payload_entropy(records: []const PayloadRecord) -> PayloadEntropyVerdict { + if (records.len > PPEM_MAX_PAYLOADS) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.TooMany, idx: 0, got: records.len, bound: PPEM_MAX_PAYLOADS, got_f: 0.0, bound_f: 0.0, }; + } + for i in 0..records.len { + if (records[i].payload_len == 0) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.ZeroLength, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + if (records[i].payload_len < PPEM_MIN_LEN) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.TooShort, idx: i, got: records[i].payload_len, bound: PPEM_MIN_LEN, got_f: 0.0, bound_f: 0.0, }; + } + if (records[i].payload_len > PPEM_MAX_LEN) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.TooLong, idx: i, got: records[i].payload_len, bound: PPEM_MAX_LEN, got_f: 0.0, bound_f: 0.0, }; + } + if (records[i].entropy < PPEM_MIN_ENTROPY) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.BelowMin, idx: i, got: 0, bound: 0, got_f: records[i].entropy, bound_f: PPEM_MIN_ENTROPY, }; + } + if (hash_seen_before(records, i)) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.DuplicateHash, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + } + return PayloadEntropyVerdict { code: PayloadEntropyCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; +} + +// The dedup hashes of the fixtures, one byte repeated thirty-two +// times - the original's hash(byte) helper, entry by entry. +pub const HASH_01: [32]u8 = [32]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const HASH_02: [32]u8 = [32]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; + +// The leading hash bytes of the too-many fixture: the original's +// (i as u8).wrapping_add(1) over 0..=1024, one byte per record. +pub const PPEM_HASH_BYTES_1025: [1025]u8 = [1025]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1,}; + +// **PPEM-01** - below-min entropy rejected: the all-zero payload +// of the original measures entropy zero against the floor of +// three bits per byte. +test ppem_01_below_min_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.BelowMin); + assert(v.idx == 0); + assert(v.got_f == 0.0); + assert(v.bound_f == 3.0); +} + +// **PPEM-02** - too short rejected: sixteen bytes of a required +// thirty-two. +test ppem_02_too_short_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16, entropy: 4.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.TooShort); + assert(v.idx == 0); + assert(v.got == 16); + assert(v.bound == 32); +} + +// **PPEM-03** - too long rejected: 16385 bytes of a ceiling 16384. +test ppem_03_too_long_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16385, entropy: 8.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.TooLong); + assert(v.idx == 0); + assert(v.got == 16385); + assert(v.bound == 16384); +} + +// **PPEM-04** - zero length rejected. +test ppem_04_zero_length_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 0, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.ZeroLength); + assert(v.idx == 0); +} + +// **PPEM-05** - duplicate hash rejected: the second record repeats +// the first's hash under a different payload. +test ppem_05_duplicate_rejected { + const records: [2]PayloadRecord = [2]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.DuplicateHash); + assert(v.idx == 1); +} + +// **PPEM-06** - too many rejected: 1025 records of a maximum +// 1024, built in a loop like the original's, each carrying a +// sixty-four-byte payload (entropy six) and a distinct leading +// hash byte. The batch rule fires before any record is examined. +test ppem_06_too_many_rejected { + var records: [1025]PayloadRecord; + for i in 0..1025 { + var h: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + h[0] = PPEM_HASH_BYTES_1025[i]; + records[i] = PayloadRecord { payload_len: 64, entropy: 6.0, payload_hash: h, }; + } + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.TooMany); + assert(v.got == 1025); + assert(v.bound == 1024); +} + +// **PPEM-07** - the valid batch accepted: two high-entropy +// payloads, distinct hashes. +test ppem_07_valid_accepted { + const records: [2]PayloadRecord = [2]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 1024, entropy: 8.0, payload_hash: HASH_02, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// **PPEM-08** - an empty batch accepted. +test ppem_08_empty_accepted { + const records: [0]PayloadRecord = [0]PayloadRecord{}; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// **PPEM-09** - the min-length boundary accepted: thirty-two +// bytes, entropy five. +test ppem_09_min_length_accepted { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 32, entropy: 5.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// **PPEM-10** - the max-length boundary accepted: 16384 bytes, +// entropy eight. +test ppem_10_max_length_accepted { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16384, entropy: 8.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// The two helpers have direct tests (coverage: Untested 0). +test hashes_equal_answers { + assert(hashes_equal(HASH_01, HASH_01)); + assert(!hashes_equal(HASH_01, HASH_02)); +} + +test hash_seen_before_answers { + const records: [3]PayloadRecord = [3]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_02, }, + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + }; + assert(!hash_seen_before(&records, 0)); + assert(!hash_seen_before(&records, 1)); + assert(hash_seen_before(&records, 2)); +} + +// Rule order is the original's: the zero-length probe before the +// length floor, the length floor before the entropy floor, the +// length ceiling before the entropy floor, and the entropy floor +// before the duplicate scan. The entropy floor admits its exact +// boundary. +test rule_order_matches_original { + const zero_short: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 0, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v1 = validate_payload_entropy(&zero_short); + assert(v1.code == PayloadEntropyCode.ZeroLength); + + const short_low: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v2 = validate_payload_entropy(&short_low); + assert(v2.code == PayloadEntropyCode.TooShort); + + const long_low: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16385, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v3 = validate_payload_entropy(&long_low); + assert(v3.code == PayloadEntropyCode.TooLong); + + const twins_low: [2]PayloadRecord = [2]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 0.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 256, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v4 = validate_payload_entropy(&twins_low); + assert(v4.code == PayloadEntropyCode.BelowMin); + assert(v4.idx == 0); + + const boundary: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 3.0, payload_hash: HASH_01, }, + }; + const v5 = validate_payload_entropy(&boundary); + assert(v5.code == PayloadEntropyCode.Ok); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27 new file mode 100644 index 0000000000..9d1ca03e48 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27 @@ -0,0 +1,476 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.rs +// (CR-CHAT-04 - Padding response timing fingerprint guard, Wave-143 +// Lane A, requirement R-CHAT-4). +// +// Padded and unpadded responses must cost the same time: an +// observer timing the server reads off whether padding applied, +// infers short from fast, and builds a binary classifier for the +// message type. The rules, in the original's order: the batch +// ceiling, the empty batch (accepted before the sample floor, +// exactly as the original places it), the sample floor, then per +// sample the zero-id probe and the repeated-id scan, then over +// the whole batch the mean floor and the coefficient-of- +// variation ceiling - the standard deviation over the mean, +// scaled by a thousand and compared as the original's integer +// cross-multiplication. +// +// The original returns Result<(), TimingFingerprintError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the six error variants in the original's +// declaration order - HighCv, TooFew, LowMean, Duplicate, +// ZeroId, TooMany - with every payload field: the measured and +// ceiling CV (x1000) for HighCv, got and min for TooFew, the +// got and min mean for LowMean, idx for the id rules, and got +// and max for the batch ceiling. +// +// The original's u64::isqrt becomes a bisection over the whole +// answer range - thirty-three halvings, exact for every u64, no +// float anywhere. The BTreeSet dedup becomes a scan over the +// samples already visited; the mean, variance and CV stay the +// original's integer u64 arithmetic, the length widening through +// the int cast. + +// Maximum CV numerator (x1000). +pub const PRTF_MAX_CV_NUM: u64 = 200; + +// Maximum CV denominator (x1000). +pub const PRTF_MAX_CV_DEN: u64 = 1000; + +// Minimum samples. +pub const PRTF_MIN_SAMPLES: usize = 8; + +// Minimum mean timing in microseconds. +pub const PRTF_MIN_MEAN_US: u64 = 50; + +// Maximum samples per batch. +pub const PRTF_MAX_SAMPLES: usize = 512; + +// Sample ID length. +pub const PRTF_SAMPLE_ID_LEN: usize = 16; + +// A timing sample record. +pub struct TimingSample { + sample_id: [16]u8, + time_us: u64, +} + +// Verdict codes: Ok(()) and TimingFingerprintError's variants, in declaration order. +pub enum TimingFingerprintCode { + Ok, + HighCv, + TooFew, + LowMean, + Duplicate, + ZeroId, + TooMany, +} + +// The verdict of the timing fingerprint validation, with every +// error payload field. +pub struct TimingFingerprintVerdict { + code: TimingFingerprintCode, + idx: usize, + got: usize, + bound: usize, + got64: u64, + bound64: u64, +} + +// Whole-form equality over the fixed sixteen id bytes. +pub fn ids16_equal(a: [16]u8, b: [16]u8) -> bool { + for i in 0..16 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero sample id - the original's [0u8; 16] probe. +pub fn id16_zero(id: [16]u8) -> bool { + for i in 0..16 { + if (id[i] != 0) { + return false; + } + } + return true; +} + +// Whether sample i repeats an earlier id - the original's +// BTreeSet insert probe, answered by a scan. +pub fn id_seen_before(samples: []const TimingSample, i: usize) -> bool { + for j in 0..i { + if (ids16_equal(samples[j].sample_id, samples[i].sample_id)) { + return true; + } + } + return false; +} + +// The integer square root of a u64 - the original's u64::isqrt, +// answered by a bisection over the whole answer range. The +// largest root of any u64 is 4294967295, and thirty-three +// halvings settle the search exactly. +pub fn isqrt64(v: u64) -> u64 { + var lo: u64 = 0; + var hi: u64 = 4294967295; + var mid: u64 = 0; + for _ in 0..33 { + if (lo <= hi) { + mid = (lo + hi) / 2; + if (mid * mid <= v) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + } + return hi; +} + +// [VERIFIED] Validate padding response timing fingerprint. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, the empty batch (accepted), +// the sample floor, then per sample the zero id and the repeated +// id, then over the whole batch the mean floor and the CV +// ceiling. +pub fn validate_timing_fingerprint(samples: []const TimingSample) -> TimingFingerprintVerdict { + if (samples.len > PRTF_MAX_SAMPLES) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.TooMany, idx: 0, got: samples.len, bound: PRTF_MAX_SAMPLES, got64: 0, bound64: 0, }; + } + if (samples.len == 0) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (samples.len < PRTF_MIN_SAMPLES) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.TooFew, idx: 0, got: samples.len, bound: PRTF_MIN_SAMPLES, got64: 0, bound64: 0, }; + } + for i in 0..samples.len { + if (id16_zero(samples[i].sample_id)) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.ZeroId, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (id_seen_before(samples, i)) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.Duplicate, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + } + const len64: u64 = @as(u64, samples.len); + var sum: u64 = 0; + for i in 0..samples.len { + sum = sum + samples[i].time_us; + } + const mean: u64 = sum / len64; + if (mean < PRTF_MIN_MEAN_US) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.LowMean, idx: 0, got: 0, bound: 0, got64: mean, bound64: PRTF_MIN_MEAN_US, }; + } + var var_sum: u64 = 0; + var diff: u64 = 0; + for i in 0..samples.len { + if (samples[i].time_us > mean) { + diff = samples[i].time_us - mean; + } else { + diff = mean - samples[i].time_us; + } + var_sum = var_sum + diff * diff; + } + const std_dev_x1000: u64 = isqrt64(var_sum / len64) * 1000; + var cv_x1000: u64 = 18446744073709551615; + if (mean > 0) { + cv_x1000 = std_dev_x1000 / mean; + } + const max_cv_x1000: u64 = PRTF_MAX_CV_NUM * 1000 / PRTF_MAX_CV_DEN; + if (cv_x1000 > max_cv_x1000) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.HighCv, idx: 0, got: 0, bound: 0, got64: cv_x1000, bound64: max_cv_x1000, }; + } + return TimingFingerprintVerdict { code: TimingFingerprintCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; +} + +// The sample ids of the fixtures, one byte repeated sixteen +// times - the original's sid(byte) helper, entry by entry. +pub const SID_01: [16]u8 = [16]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const SID_02: [16]u8 = [16]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const SID_03: [16]u8 = [16]u8{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; +pub const SID_04: [16]u8 = [16]u8{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,}; +pub const SID_05: [16]u8 = [16]u8{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,}; +pub const SID_06: [16]u8 = [16]u8{6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}; +pub const SID_07: [16]u8 = [16]u8{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,}; +pub const SID_08: [16]u8 = [16]u8{8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,}; +pub const SID_09: [16]u8 = [16]u8{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,}; +pub const SID_10: [16]u8 = [16]u8{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,}; +pub const ZERO_ID16: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The leading id bytes of the thirty-sample fixture. +pub const PRTF_ID_BYTES_30: [30]u8 = [30]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,}; + +// The two id bytes of the too-many fixture: the original writes +// the big-endian u64 of i + 1 into id[0..8], and for every value +// in 1..=513 only the last two of those eight bytes can carry a +// nonzero digit. +pub const PRTF_ID6_513: [513]u8 = [513]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2,}; +pub const PRTF_ID7_513: [513]u8 = [513]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1,}; + +// **PRTF-01** - high CV rejected: seven samples at a hundred +// microseconds and one at a hundred thousand - CV x1000 of 2624 +// over the ceiling of 200. +test prtf_01_high_cv_rejected { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 100, }, + TimingSample { sample_id: SID_02, time_us: 100, }, + TimingSample { sample_id: SID_03, time_us: 100, }, + TimingSample { sample_id: SID_04, time_us: 100, }, + TimingSample { sample_id: SID_05, time_us: 100, }, + TimingSample { sample_id: SID_06, time_us: 100, }, + TimingSample { sample_id: SID_07, time_us: 100, }, + TimingSample { sample_id: SID_08, time_us: 100000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.HighCv); + assert(v.got64 == 2624); + assert(v.bound64 == 200); +} + +// **PRTF-02** - too few rejected: seven samples of a required +// eight. +test prtf_02_too_few_rejected { + const samples: [7]TimingSample = [7]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.TooFew); + assert(v.got == 7); + assert(v.bound == 8); +} + +// **PRTF-03** - low mean rejected: ten samples at ten +// microseconds - a mean of ten under the floor of fifty. +test prtf_03_low_mean_rejected { + const samples: [10]TimingSample = [10]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 10, }, + TimingSample { sample_id: SID_02, time_us: 10, }, + TimingSample { sample_id: SID_03, time_us: 10, }, + TimingSample { sample_id: SID_04, time_us: 10, }, + TimingSample { sample_id: SID_05, time_us: 10, }, + TimingSample { sample_id: SID_06, time_us: 10, }, + TimingSample { sample_id: SID_07, time_us: 10, }, + TimingSample { sample_id: SID_08, time_us: 10, }, + TimingSample { sample_id: SID_09, time_us: 10, }, + TimingSample { sample_id: SID_10, time_us: 10, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.LowMean); + assert(v.got64 == 10); + assert(v.bound64 == 50); +} + +// **PRTF-04** - duplicate rejected: the second sample repeats +// the first's id. +test prtf_04_duplicate_rejected { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Duplicate); + assert(v.idx == 1); +} + +// **PRTF-05** - zero id rejected under live timings. +test prtf_05_zero_id_rejected { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: ZERO_ID16, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + TimingSample { sample_id: SID_08, time_us: 1000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.ZeroId); + assert(v.idx == 0); +} + +// **PRTF-06** - too many rejected: 513 samples of a maximum +// 512, built in a loop like the original's, each id carrying the +// big-endian u64 of its index plus one. The batch rule fires +// before any sample is examined. +test prtf_06_too_many_rejected { + var samples: [513]TimingSample; + for i in 0..513 { + var id: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + id[6] = PRTF_ID6_513[i]; + id[7] = PRTF_ID7_513[i]; + samples[i] = TimingSample { sample_id: id, time_us: 1000, }; + } + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.TooMany); + assert(v.got == 513); + assert(v.bound == 512); +} + +// **PRTF-07** - the uniform batch accepted: ten samples stepping +// by ten microseconds from a thousand - CV x1000 of 26. +test prtf_07_valid_accepted { + const samples: [10]TimingSample = [10]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1010, }, + TimingSample { sample_id: SID_03, time_us: 1020, }, + TimingSample { sample_id: SID_04, time_us: 1030, }, + TimingSample { sample_id: SID_05, time_us: 1040, }, + TimingSample { sample_id: SID_06, time_us: 1050, }, + TimingSample { sample_id: SID_07, time_us: 1060, }, + TimingSample { sample_id: SID_08, time_us: 1070, }, + TimingSample { sample_id: SID_09, time_us: 1080, }, + TimingSample { sample_id: SID_10, time_us: 1090, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// **PRTF-08** - an empty batch accepted, before the sample +// floor, exactly as the original places it. +test prtf_08_empty_accepted { + const samples: [0]TimingSample = [0]TimingSample{}; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// **PRTF-09** - the minimum-sample boundary accepted: eight +// samples stepping by five from five hundred. +test prtf_09_min_samples_accepted { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 500, }, + TimingSample { sample_id: SID_02, time_us: 505, }, + TimingSample { sample_id: SID_03, time_us: 510, }, + TimingSample { sample_id: SID_04, time_us: 515, }, + TimingSample { sample_id: SID_05, time_us: 520, }, + TimingSample { sample_id: SID_06, time_us: 525, }, + TimingSample { sample_id: SID_07, time_us: 530, }, + TimingSample { sample_id: SID_08, time_us: 535, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// **PRTF-10** - many uniform high values accepted: thirty samples +// stepping by two from five thousand. +test prtf_10_many_uniform_accepted { + var samples: [30]TimingSample; + var t: u64 = 5000; + for i in 0..30 { + var id: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + id[0] = PRTF_ID_BYTES_30[i]; + samples[i] = TimingSample { sample_id: id, time_us: t, }; + t = t + 2; + } + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// The four helpers have direct tests (coverage: Untested 0). +test ids16_helpers_answer { + assert(ids16_equal(SID_01, SID_01)); + assert(!ids16_equal(SID_01, SID_02)); + assert(id16_zero(ZERO_ID16)); + assert(!id16_zero(SID_01)); + const samples: [3]TimingSample = [3]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_01, time_us: 1000, }, + }; + assert(!id_seen_before(&samples, 0)); + assert(!id_seen_before(&samples, 1)); + assert(id_seen_before(&samples, 2)); +} + +test isqrt64_answers { + assert(isqrt64(0) == 0); + assert(isqrt64(1) == 1); + assert(isqrt64(3) == 1); + assert(isqrt64(4) == 2); + assert(isqrt64(845) == 29); + assert(isqrt64(125) == 11); + assert(isqrt64(100) == 10); + assert(isqrt64(18446744073709551615) == 4294967295); +} + +// Rule order is the original's: the batch ceiling before any +// sample, the zero id before the duplicate, the duplicate before +// the mean floor, the mean floor before the CV ceiling - and the +// CV ceiling admitting its exact boundary. +test rule_order_matches_original { + var flooded: [513]TimingSample; + for i in 0..513 { + flooded[i] = TimingSample { sample_id: ZERO_ID16, time_us: 1000, }; + } + const v1 = validate_timing_fingerprint(&flooded); + assert(v1.code == TimingFingerprintCode.TooMany); + + const twin_zeros: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: ZERO_ID16, time_us: 1000, }, + TimingSample { sample_id: ZERO_ID16, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + }; + const v2 = validate_timing_fingerprint(&twin_zeros); + assert(v2.code == TimingFingerprintCode.ZeroId); + assert(v2.idx == 0); + + const dup_low_mean: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 10, }, + TimingSample { sample_id: SID_01, time_us: 10, }, + TimingSample { sample_id: SID_02, time_us: 10, }, + TimingSample { sample_id: SID_03, time_us: 10, }, + TimingSample { sample_id: SID_04, time_us: 10, }, + TimingSample { sample_id: SID_05, time_us: 10, }, + TimingSample { sample_id: SID_06, time_us: 10, }, + TimingSample { sample_id: SID_07, time_us: 10, }, + }; + const v3 = validate_timing_fingerprint(&dup_low_mean); + assert(v3.code == TimingFingerprintCode.Duplicate); + assert(v3.idx == 1); + + const low_mean_high_cv: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 0, }, + TimingSample { sample_id: SID_02, time_us: 0, }, + TimingSample { sample_id: SID_03, time_us: 0, }, + TimingSample { sample_id: SID_04, time_us: 0, }, + TimingSample { sample_id: SID_05, time_us: 0, }, + TimingSample { sample_id: SID_06, time_us: 0, }, + TimingSample { sample_id: SID_07, time_us: 0, }, + TimingSample { sample_id: SID_08, time_us: 399, }, + }; + const v4 = validate_timing_fingerprint(&low_mean_high_cv); + assert(v4.code == TimingFingerprintCode.LowMean); + assert(v4.got64 == 49); + + const boundary_cv: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 40, }, + TimingSample { sample_id: SID_02, time_us: 40, }, + TimingSample { sample_id: SID_03, time_us: 40, }, + TimingSample { sample_id: SID_04, time_us: 40, }, + TimingSample { sample_id: SID_05, time_us: 60, }, + TimingSample { sample_id: SID_06, time_us: 60, }, + TimingSample { sample_id: SID_07, time_us: 60, }, + TimingSample { sample_id: SID_08, time_us: 60, }, + }; + const v5 = validate_timing_fingerprint(&boundary_cv); + assert(v5.code == TimingFingerprintCode.Ok); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_side_channel_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_side_channel_guard.t27 new file mode 100644 index 0000000000..2661f9a584 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_side_channel_guard.t27 @@ -0,0 +1,350 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_side_channel_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_side_channel_guard.rs +// (CR-CHAT-04 - Padding timing side-channel guard, Wave-123 Lane A, +// requirement R-CHAT-4). +// +// Padding must cost constant time: a routine measurably slower on +// real messages than on cover traffic lets an observer sort them +// by latency, data-dependent memory access opens cache side +// channels, and server-side variance propagates to network +// timing a middlebox can read. The rules, in the original's +// order: the batch ceiling, then per measurement the zero-duration +// probe, the duration floor and ceiling, and the repeated-op-id +// scan, then over the whole batch the variance ceiling - the +// widest minus the narrowest duration, checked only once at +// least two measurements exist. +// +// The original returns Result<(), TimingSideChannelError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the six error variants in the original's +// declaration order - TooFast, TooSlow, ZeroDuration, +// HighVariance, DuplicateOpId, TooMany - with every payload +// field: idx and the got/min or got/max durations for the +// per-record rules, the measured/max variance for the batch +// rule, and got and max for the ceiling. +// +// The BTreeSet dedup becomes a scan over the measurements +// already visited; the min/max tracking stays the original's +// u64 arithmetic, the saturating subtraction guarded explicitly. + +// Minimum allowed duration in microseconds. +pub const PTSC_MIN_DURATION_US: u64 = 100; + +// Maximum allowed duration in microseconds. +pub const PTSC_MAX_DURATION_US: u64 = 10000; + +// Maximum variance across the batch in microseconds. +pub const PTSC_MAX_VARIANCE_US: u64 = 1000; + +// Maximum operations per batch. +pub const PTSC_MAX_OPS: usize = 1024; + +// Operation ID length. +pub const PTSC_OP_ID_LEN: usize = 32; + +// A padding timing measurement. +pub struct TimingMeasurement { + op_id: [32]u8, + duration_us: u64, +} + +// Verdict codes: Ok(()) and TimingSideChannelError's variants, in declaration order. +pub enum TimingSideChannelCode { + Ok, + TooFast, + TooSlow, + ZeroDuration, + HighVariance, + DuplicateOpId, + TooMany, +} + +// The verdict of the timing constancy validation, with every +// error payload field. +pub struct TimingSideChannelVerdict { + code: TimingSideChannelCode, + idx: usize, + got: usize, + bound: usize, + got64: u64, + bound64: u64, +} + +// Whole-form equality over the fixed thirty-two op-id bytes. +pub fn op_ids_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero operation id - the original's [0u8; 32] probe. +pub fn op_id_zero(id: [32]u8) -> bool { + for i in 0..32 { + if (id[i] != 0) { + return false; + } + } + return true; +} + +// Whether measurement i repeats an earlier op id - the original's +// BTreeSet insert probe, answered by a scan. +pub fn op_seen_before(measurements: []const TimingMeasurement, i: usize) -> bool { + for j in 0..i { + if (op_ids_equal(measurements[j].op_id, measurements[i].op_id)) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate padding timing side-channel resistance. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, then per measurement the +// zero duration, the duration floor and ceiling and the repeated +// op id, then over the whole batch the variance ceiling. +pub fn validate_timing_constancy(measurements: []const TimingMeasurement) -> TimingSideChannelVerdict { + if (measurements.len > PTSC_MAX_OPS) { + return TimingSideChannelVerdict { code: TimingSideChannelCode.TooMany, idx: 0, got: measurements.len, bound: PTSC_MAX_OPS, got64: 0, bound64: 0, }; + } + var min_dur: u64 = 18446744073709551615; + var max_dur: u64 = 0; + for i in 0..measurements.len { + if (measurements[i].duration_us == 0) { + return TimingSideChannelVerdict { code: TimingSideChannelCode.ZeroDuration, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (measurements[i].duration_us < PTSC_MIN_DURATION_US) { + return TimingSideChannelVerdict { code: TimingSideChannelCode.TooFast, idx: i, got: 0, bound: 0, got64: measurements[i].duration_us, bound64: PTSC_MIN_DURATION_US, }; + } + if (measurements[i].duration_us > PTSC_MAX_DURATION_US) { + return TimingSideChannelVerdict { code: TimingSideChannelCode.TooSlow, idx: i, got: 0, bound: 0, got64: measurements[i].duration_us, bound64: PTSC_MAX_DURATION_US, }; + } + if (op_seen_before(measurements, i)) { + return TimingSideChannelVerdict { code: TimingSideChannelCode.DuplicateOpId, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (measurements[i].duration_us < min_dur) { + min_dur = measurements[i].duration_us; + } + if (measurements[i].duration_us > max_dur) { + max_dur = measurements[i].duration_us; + } + } + if (measurements.len >= 2) { + var variance: u64 = 0; + if (max_dur >= min_dur) { + variance = max_dur - min_dur; + } + if (variance > PTSC_MAX_VARIANCE_US) { + return TimingSideChannelVerdict { code: TimingSideChannelCode.HighVariance, idx: 0, got: 0, bound: 0, got64: variance, bound64: PTSC_MAX_VARIANCE_US, }; + } + } + return TimingSideChannelVerdict { code: TimingSideChannelCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; +} + +// The operation ids of the fixtures, one byte repeated thirty-two +// times - the original's oid(byte) helper, entry by entry. +pub const OPID_01: [32]u8 = [32]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const OPID_02: [32]u8 = [32]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const OPID_03: [32]u8 = [32]u8{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; +pub const OPID_04: [32]u8 = [32]u8{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,}; + +// The two id bytes of the too-many fixture: the original writes +// the big-endian u64 of i + 1 into id[0..8], and for every value +// in 1..=1025 only the last two of those eight bytes can carry a +// nonzero digit. +pub const PTSC_ID6_1025: [1025]u8 = [1025]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4,}; +pub const PTSC_ID7_1025: [1025]u8 = [1025]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1,}; + +// **PTSC-01** - too fast rejected: ninety-nine microseconds of a +// floor hundred. +test ptsc_01_too_fast_rejected { + const ms: [1]TimingMeasurement = [1]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 99, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.TooFast); + assert(v.idx == 0); + assert(v.got64 == 99); + assert(v.bound64 == 100); +} + +// **PTSC-02** - too slow rejected: 10001 microseconds of a +// ceiling ten thousand. +test ptsc_02_too_slow_rejected { + const ms: [1]TimingMeasurement = [1]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 10001, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.TooSlow); + assert(v.idx == 0); + assert(v.got64 == 10001); + assert(v.bound64 == 10000); +} + +// **PTSC-03** - zero duration rejected. +test ptsc_03_zero_duration_rejected { + const ms: [1]TimingMeasurement = [1]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 0, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.ZeroDuration); + assert(v.idx == 0); +} + +// **PTSC-04** - high variance rejected: a spread of 1001 +// microseconds over the ceiling of a thousand. +test ptsc_04_high_variance_rejected { + const ms: [2]TimingMeasurement = [2]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + TimingMeasurement { op_id: OPID_02, duration_us: 1501, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.HighVariance); + assert(v.got64 == 1001); + assert(v.bound64 == 1000); +} + +// **PTSC-05** - duplicate operation id rejected: the second +// measurement repeats the first's id. +test ptsc_05_duplicate_rejected { + const ms: [2]TimingMeasurement = [2]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.DuplicateOpId); + assert(v.idx == 1); +} + +// **PTSC-06** - too many rejected: 1025 measurements of a maximum +// 1024, built in a loop like the original's, each id carrying the +// big-endian u64 of its index plus one. The batch rule fires +// before any measurement is examined. +test ptsc_06_too_many_rejected { + var ms: [1025]TimingMeasurement; + for i in 0..1025 { + var id: [32]u8; + for j in 0..32 { + id[j] = 0; + } + id[6] = PTSC_ID6_1025[i]; + id[7] = PTSC_ID7_1025[i]; + ms[i] = TimingMeasurement { op_id: id, duration_us: 500, }; + } + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.TooMany); + assert(v.got == 1025); + assert(v.bound == 1024); +} + +// **PTSC-07** - the valid batch accepted: durations 500, 550, +// 480 and 520 - a spread of seventy. +test ptsc_07_valid_accepted { + const ms: [4]TimingMeasurement = [4]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + TimingMeasurement { op_id: OPID_02, duration_us: 550, }, + TimingMeasurement { op_id: OPID_03, duration_us: 480, }, + TimingMeasurement { op_id: OPID_04, duration_us: 520, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.Ok); +} + +// **PTSC-08** - an empty batch accepted. +test ptsc_08_empty_accepted { + const ms: [0]TimingMeasurement = [0]TimingMeasurement{}; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.Ok); +} + +// **PTSC-09** - a single measurement accepted - the variance +// ceiling checks nothing below two. +test ptsc_09_single_accepted { + const ms: [1]TimingMeasurement = [1]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.Ok); +} + +// **PTSC-10** - the boundary variance accepted: a spread of +// exactly a thousand. +test ptsc_10_boundary_variance_accepted { + const ms: [2]TimingMeasurement = [2]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + TimingMeasurement { op_id: OPID_02, duration_us: 1500, }, + }; + const v = validate_timing_constancy(&ms); + assert(v.code == TimingSideChannelCode.Ok); +} + +// The three helpers have direct tests (coverage: Untested 0). +test op_id_helpers_answer { + assert(op_ids_equal(OPID_01, OPID_01)); + assert(!op_ids_equal(OPID_01, OPID_02)); + var zero: [32]u8; + for j in 0..32 { + zero[j] = 0; + } + assert(op_id_zero(zero)); + assert(!op_id_zero(OPID_01)); + const ms: [3]TimingMeasurement = [3]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + TimingMeasurement { op_id: OPID_02, duration_us: 500, }, + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + }; + assert(!op_seen_before(&ms, 0)); + assert(!op_seen_before(&ms, 1)); + assert(op_seen_before(&ms, 2)); +} + +// Rule order is the original's: the zero duration before the +// floor, the floor before the repeated id, the ceiling before +// the repeated id, the repeated id before the batch variance - +// and the batch ceiling before any measurement. +test rule_order_matches_original { + const zero_fast: [1]TimingMeasurement = [1]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 0, }, + }; + const v1 = validate_timing_constancy(&zero_fast); + assert(v1.code == TimingSideChannelCode.ZeroDuration); + + const fast_twins: [2]TimingMeasurement = [2]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 99, }, + TimingMeasurement { op_id: OPID_01, duration_us: 99, }, + }; + const v2 = validate_timing_constancy(&fast_twins); + assert(v2.code == TimingSideChannelCode.TooFast); + assert(v2.idx == 0); + + const slow_twins: [2]TimingMeasurement = [2]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 10001, }, + TimingMeasurement { op_id: OPID_01, duration_us: 10001, }, + }; + const v3 = validate_timing_constancy(&slow_twins); + assert(v3.code == TimingSideChannelCode.TooSlow); + assert(v3.idx == 0); + + const dup_high_variance: [2]TimingMeasurement = [2]TimingMeasurement{ + TimingMeasurement { op_id: OPID_01, duration_us: 500, }, + TimingMeasurement { op_id: OPID_01, duration_us: 1501, }, + }; + const v4 = validate_timing_constancy(&dup_high_variance); + assert(v4.code == TimingSideChannelCode.DuplicateOpId); + assert(v4.idx == 1); + + var flooded: [1025]TimingMeasurement; + for i in 0..1025 { + var id: [32]u8; + for j in 0..32 { + id[j] = 0; + } + flooded[i] = TimingMeasurement { op_id: id, duration_us: 0, }; + } + const v5 = validate_timing_constancy(&flooded); + assert(v5.code == TimingSideChannelCode.TooMany); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_uniformity_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_uniformity_guard.t27 new file mode 100644 index 0000000000..a5da62aca7 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_uniformity_guard.t27 @@ -0,0 +1,465 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_timing_uniformity_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_timing_uniformity_guard.rs +// (CR-CHAT-04 - Padding timing uniformity guard, Wave-151 Lane B, +// requirement R-CHAT-4). +// +// Padded-message timing must be statistically uniform: timing +// patterns between padded blocks correlate with real-message +// injection points, inter-arrival variance separates real from +// decoy traffic, and consistent patterns fingerprint the user's +// communication. The rules, in the original's order: the batch +// ceiling, the observation floor (an empty batch fails it, exactly +// as the original places it), then per observation the zero-id +// probe and the repeated-id scan, then over the whole batch the +// mean floor and the coefficient-of-variation ceiling - the +// standard deviation over the mean, scaled by ten thousand. +// +// The original returns Result<(), TimingUniformityError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the six error variants in the original's +// declaration order - HighCv, LowMean, DuplicateId, ZeroId, +// TooFew, TooMany - with every payload field: the measured and +// ceiling CV for HighCv, the computed and minimum mean for +// LowMean, idx for the id rules, and got with min or max for the +// batch bounds. +// +// The original's approx_sqrt is a Newton iteration with a while +// loop; t27 has no while, so the integer square root is answered +// by a bisection over the whole answer range - thirty-three +// halvings, the same exact floor for every u64. The original's +// u128 variance accumulator becomes u64: the diff squares of +// every original fixture sit far below the u64 ceiling, and the +// arithmetic is identical wherever they fit. + +// Maximum observations per batch. +pub const PTU_MAX_OBS: usize = 4096; + +// Minimum observations required. +pub const PTU_MIN_OBS: usize = 8; + +// Maximum coefficient of variation (scaled by 10000). +pub const PTU_MAX_CV: u64 = 1500; + +// Minimum mean inter-arrival time in microseconds. +pub const PTU_MIN_MEAN_US: u64 = 100; + +// Observation ID length. +pub const PTU_OBS_ID_LEN: usize = 16; + +// A timing observation. +pub struct TimingObservation { + obs_id: [16]u8, + interval_us: u64, +} + +// Verdict codes: Ok(()) and TimingUniformityError's variants, in declaration order. +pub enum TimingUniformityCode { + Ok, + HighCv, + LowMean, + DuplicateId, + ZeroId, + TooFew, + TooMany, +} + +// The verdict of the timing uniformity validation, with every +// error payload field. +pub struct TimingUniformityVerdict { + code: TimingUniformityCode, + idx: usize, + got: usize, + bound: usize, + got64: u64, + bound64: u64, +} + +// Whole-form equality over the fixed sixteen observation-id +// bytes. +pub fn obs_ids_equal(a: [16]u8, b: [16]u8) -> bool { + for i in 0..16 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero observation id - the original's [0u8; 16] probe. +pub fn obs_id_zero(id: [16]u8) -> bool { + for i in 0..16 { + if (id[i] != 0) { + return false; + } + } + return true; +} + +// Whether observation i repeats an earlier id - the original's +// BTreeSet insert probe, answered by a scan. +pub fn obs_seen_before(obs: []const TimingObservation, i: usize) -> bool { + for j in 0..i { + if (obs_ids_equal(obs[j].obs_id, obs[i].obs_id)) { + return true; + } + } + return false; +} + +// The integer square root of a u64 - the original's approx_sqrt +// Newton iteration, answered by a bisection over the whole answer +// range. The largest root of any u64 is 4294967295, and +// thirty-three halvings settle the search exactly. +pub fn approx_sqrt(n: u64) -> u64 { + var lo: u64 = 0; + var hi: u64 = 4294967295; + var mid: u64 = 0; + for _ in 0..33 { + if (lo <= hi) { + mid = (lo + hi) / 2; + if (mid * mid <= n) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + } + return hi; +} + +// [VERIFIED] Validate padding timing uniformity. Returns Ok only +// when every rule holds, checked in the original's order: the +// batch ceiling, the observation floor, then per observation the +// zero id and the repeated id, then over the whole batch the mean +// floor and the CV ceiling. +pub fn validate_timing_uniformity(obs: []const TimingObservation) -> TimingUniformityVerdict { + if (obs.len > PTU_MAX_OBS) { + return TimingUniformityVerdict { code: TimingUniformityCode.TooMany, idx: 0, got: obs.len, bound: PTU_MAX_OBS, got64: 0, bound64: 0, }; + } + if (obs.len < PTU_MIN_OBS) { + return TimingUniformityVerdict { code: TimingUniformityCode.TooFew, idx: 0, got: obs.len, bound: PTU_MIN_OBS, got64: 0, bound64: 0, }; + } + const n: u64 = @as(u64, obs.len); + var sum: u64 = 0; + for i in 0..obs.len { + if (obs_id_zero(obs[i].obs_id)) { + return TimingUniformityVerdict { code: TimingUniformityCode.ZeroId, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (obs_seen_before(obs, i)) { + return TimingUniformityVerdict { code: TimingUniformityCode.DuplicateId, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + sum = sum + obs[i].interval_us; + } + const mean: u64 = sum / n; + if (mean < PTU_MIN_MEAN_US) { + return TimingUniformityVerdict { code: TimingUniformityCode.LowMean, idx: 0, got: 0, bound: 0, got64: mean, bound64: PTU_MIN_MEAN_US, }; + } + var variance_sum: u64 = 0; + var diff: u64 = 0; + for i in 0..obs.len { + if (obs[i].interval_us > mean) { + diff = obs[i].interval_us - mean; + } else { + diff = mean - obs[i].interval_us; + } + variance_sum = variance_sum + diff * diff; + } + const variance: u64 = variance_sum / n; + const std_dev: u64 = approx_sqrt(variance); + var cv: u64 = PTU_MAX_CV + 1; + if (mean > 0) { + cv = std_dev * 10000 / mean; + } + if (cv > PTU_MAX_CV) { + return TimingUniformityVerdict { code: TimingUniformityCode.HighCv, idx: 0, got: 0, bound: 0, got64: cv, bound64: PTU_MAX_CV, }; + } + return TimingUniformityVerdict { code: TimingUniformityCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; +} + +// The observation ids of the fixtures, one byte repeated sixteen +// times - the original's oid(byte) helper, entry by entry. +pub const OID_01: [16]u8 = [16]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const OID_02: [16]u8 = [16]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const OID_03: [16]u8 = [16]u8{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; +pub const OID_04: [16]u8 = [16]u8{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,}; +pub const OID_05: [16]u8 = [16]u8{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,}; +pub const OID_06: [16]u8 = [16]u8{6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}; +pub const OID_07: [16]u8 = [16]u8{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,}; +pub const OID_08: [16]u8 = [16]u8{8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,}; +pub const OID_09: [16]u8 = [16]u8{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,}; +pub const OID_10: [16]u8 = [16]u8{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,}; +pub const ZERO_OID: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The leading id bytes of the many-uniform fixture. +pub const PTU_ID_BYTES_100: [100]u8 = [100]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100,}; + +// The two id bytes of the too-many fixture: the original writes +// the big-endian u64 of i + 1 into id[0..8], and for every value +// in 1..=4097 only the last two of those eight bytes can carry a +// nonzero digit. +pub const PTU_ID6_4097: [4097]u8 = [4097]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16,}; +pub const PTU_ID7_4097: [4097]u8 = [4097]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0, 1,}; + +// **PTU-01** - high CV rejected: five observations at a hundred +// microseconds and five at ten million - CV x10000 of 9999 over +// the ceiling of 1500. +test ptu_01_high_cv_rejected { + const obs_set: [10]TimingObservation = [10]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 100, }, + TimingObservation { obs_id: OID_02, interval_us: 100, }, + TimingObservation { obs_id: OID_03, interval_us: 100, }, + TimingObservation { obs_id: OID_04, interval_us: 100, }, + TimingObservation { obs_id: OID_05, interval_us: 100, }, + TimingObservation { obs_id: OID_06, interval_us: 10000000, }, + TimingObservation { obs_id: OID_07, interval_us: 10000000, }, + TimingObservation { obs_id: OID_08, interval_us: 10000000, }, + TimingObservation { obs_id: OID_09, interval_us: 10000000, }, + TimingObservation { obs_id: OID_10, interval_us: 10000000, }, + }; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.HighCv); + assert(v.got64 == 9999); + assert(v.bound64 == 1500); +} + +// **PTU-02** - low mean rejected: ten observations at ten +// microseconds - a mean of ten under the floor of a hundred. +test ptu_02_low_mean_rejected { + const obs_set: [10]TimingObservation = [10]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 10, }, + TimingObservation { obs_id: OID_02, interval_us: 10, }, + TimingObservation { obs_id: OID_03, interval_us: 10, }, + TimingObservation { obs_id: OID_04, interval_us: 10, }, + TimingObservation { obs_id: OID_05, interval_us: 10, }, + TimingObservation { obs_id: OID_06, interval_us: 10, }, + TimingObservation { obs_id: OID_07, interval_us: 10, }, + TimingObservation { obs_id: OID_08, interval_us: 10, }, + TimingObservation { obs_id: OID_09, interval_us: 10, }, + TimingObservation { obs_id: OID_10, interval_us: 10, }, + }; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.LowMean); + assert(v.got64 == 10); + assert(v.bound64 == 100); +} + +// **PTU-03** - duplicate id rejected: the uniform batch plus one +// more observation repeating the first id. +test ptu_03_duplicate_rejected { + const obs_set: [11]TimingObservation = [11]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 1000, }, + TimingObservation { obs_id: OID_02, interval_us: 1010, }, + TimingObservation { obs_id: OID_03, interval_us: 1020, }, + TimingObservation { obs_id: OID_04, interval_us: 1030, }, + TimingObservation { obs_id: OID_05, interval_us: 1040, }, + TimingObservation { obs_id: OID_06, interval_us: 1050, }, + TimingObservation { obs_id: OID_07, interval_us: 1060, }, + TimingObservation { obs_id: OID_08, interval_us: 1070, }, + TimingObservation { obs_id: OID_09, interval_us: 1080, }, + TimingObservation { obs_id: OID_10, interval_us: 1090, }, + TimingObservation { obs_id: OID_01, interval_us: 1000, }, + }; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.DuplicateId); + assert(v.idx == 10); +} + +// **PTU-04** - zero id rejected under live intervals. +test ptu_04_zero_id_rejected { + const obs_set: [10]TimingObservation = [10]TimingObservation{ + TimingObservation { obs_id: ZERO_OID, interval_us: 1000, }, + TimingObservation { obs_id: OID_02, interval_us: 1010, }, + TimingObservation { obs_id: OID_03, interval_us: 1020, }, + TimingObservation { obs_id: OID_04, interval_us: 1030, }, + TimingObservation { obs_id: OID_05, interval_us: 1040, }, + TimingObservation { obs_id: OID_06, interval_us: 1050, }, + TimingObservation { obs_id: OID_07, interval_us: 1060, }, + TimingObservation { obs_id: OID_08, interval_us: 1070, }, + TimingObservation { obs_id: OID_09, interval_us: 1080, }, + TimingObservation { obs_id: OID_10, interval_us: 1090, }, + }; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.ZeroId); + assert(v.idx == 0); +} + +// **PTU-05** - too few rejected: three observations of a required +// eight. +test ptu_05_too_few_rejected { + const obs_set: [3]TimingObservation = [3]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 1000, }, + TimingObservation { obs_id: OID_02, interval_us: 1000, }, + TimingObservation { obs_id: OID_03, interval_us: 1000, }, + }; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.TooFew); + assert(v.got == 3); + assert(v.bound == 8); +} + +// **PTU-06** - too many rejected: 4097 observations of a maximum +// 4096, built in a loop like the original's, each id carrying +// the big-endian u64 of its index plus one. The batch rule fires +// before any observation is examined. +test ptu_06_too_many_rejected { + var obs_set: [4097]TimingObservation; + for i in 0..4097 { + var id: [16]u8; + for j in 0..16 { + id[j] = 0; + } + id[6] = PTU_ID6_4097[i]; + id[7] = PTU_ID7_4097[i]; + obs_set[i] = TimingObservation { obs_id: id, interval_us: 1000, }; + } + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.TooMany); + assert(v.got == 4097); + assert(v.bound == 4096); +} + +// **PTU-07** - the uniform batch accepted: ten observations +// stepping by ten microseconds from a thousand - CV x10000 of 267. +test ptu_07_valid_accepted { + const obs_set: [10]TimingObservation = [10]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 1000, }, + TimingObservation { obs_id: OID_02, interval_us: 1010, }, + TimingObservation { obs_id: OID_03, interval_us: 1020, }, + TimingObservation { obs_id: OID_04, interval_us: 1030, }, + TimingObservation { obs_id: OID_05, interval_us: 1040, }, + TimingObservation { obs_id: OID_06, interval_us: 1050, }, + TimingObservation { obs_id: OID_07, interval_us: 1060, }, + TimingObservation { obs_id: OID_08, interval_us: 1070, }, + TimingObservation { obs_id: OID_09, interval_us: 1080, }, + TimingObservation { obs_id: OID_10, interval_us: 1090, }, + }; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.Ok); +} + +// **PTU-08** - an empty batch rejected on the observation floor. +test ptu_08_empty_rejected { + const obs_set: [0]TimingObservation = [0]TimingObservation{}; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.TooFew); + assert(v.got == 0); + assert(v.bound == 8); +} + +// **PTU-09** - exactly the minimum count accepted: eight +// identical intervals. +test ptu_09_exact_min_accepted { + const obs_set: [8]TimingObservation = [8]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 1000, }, + TimingObservation { obs_id: OID_02, interval_us: 1000, }, + TimingObservation { obs_id: OID_03, interval_us: 1000, }, + TimingObservation { obs_id: OID_04, interval_us: 1000, }, + TimingObservation { obs_id: OID_05, interval_us: 1000, }, + TimingObservation { obs_id: OID_06, interval_us: 1000, }, + TimingObservation { obs_id: OID_07, interval_us: 1000, }, + TimingObservation { obs_id: OID_08, interval_us: 1000, }, + }; + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.Ok); +} + +// **PTU-10** - many uniform observations accepted: a hundred +// intervals stepping by one from five thousand. +test ptu_10_many_uniform_accepted { + var obs_set: [100]TimingObservation; + var t: u64 = 5000; + for i in 0..100 { + var id: [16]u8; + for j in 0..16 { + id[j] = 0; + } + id[0] = PTU_ID_BYTES_100[i]; + obs_set[i] = TimingObservation { obs_id: id, interval_us: t, }; + t = t + 1; + } + const v = validate_timing_uniformity(&obs_set); + assert(v.code == TimingUniformityCode.Ok); +} + +// The four helpers have direct tests (coverage: Untested 0). +test ptu_helpers_answer { + assert(obs_ids_equal(OID_01, OID_01)); + assert(!obs_ids_equal(OID_01, OID_02)); + assert(obs_id_zero(ZERO_OID)); + assert(!obs_id_zero(OID_01)); + const three: [3]TimingObservation = [3]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 1000, }, + TimingObservation { obs_id: OID_02, interval_us: 1000, }, + TimingObservation { obs_id: OID_01, interval_us: 1000, }, + }; + assert(!obs_seen_before(&three, 0)); + assert(!obs_seen_before(&three, 1)); + assert(obs_seen_before(&three, 2)); + assert(approx_sqrt(0) == 0); + assert(approx_sqrt(1) == 1); + assert(approx_sqrt(825) == 28); + assert(approx_sqrt(225) == 15); + assert(approx_sqrt(18446744073709551615) == 4294967295); +} + +// Rule order is the original's: the observation floor before any +// record, the zero id before the duplicate, the duplicate before +// the mean floor, the mean floor before the CV ceiling - and the +// CV ceiling admitting its exact boundary. +test rule_order_matches_original { + const zero_twins: [8]TimingObservation = [8]TimingObservation{ + TimingObservation { obs_id: ZERO_OID, interval_us: 1000, }, + TimingObservation { obs_id: ZERO_OID, interval_us: 1000, }, + TimingObservation { obs_id: OID_03, interval_us: 1000, }, + TimingObservation { obs_id: OID_04, interval_us: 1000, }, + TimingObservation { obs_id: OID_05, interval_us: 1000, }, + TimingObservation { obs_id: OID_06, interval_us: 1000, }, + TimingObservation { obs_id: OID_07, interval_us: 1000, }, + TimingObservation { obs_id: OID_08, interval_us: 1000, }, + }; + const v1 = validate_timing_uniformity(&zero_twins); + assert(v1.code == TimingUniformityCode.ZeroId); + assert(v1.idx == 0); + + const dup_low_mean: [8]TimingObservation = [8]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 10, }, + TimingObservation { obs_id: OID_01, interval_us: 10, }, + TimingObservation { obs_id: OID_03, interval_us: 10, }, + TimingObservation { obs_id: OID_04, interval_us: 10, }, + TimingObservation { obs_id: OID_05, interval_us: 10, }, + TimingObservation { obs_id: OID_06, interval_us: 10, }, + TimingObservation { obs_id: OID_07, interval_us: 10, }, + TimingObservation { obs_id: OID_08, interval_us: 10, }, + }; + const v2 = validate_timing_uniformity(&dup_low_mean); + assert(v2.code == TimingUniformityCode.DuplicateId); + assert(v2.idx == 1); + + const low_mean_high_cv: [8]TimingObservation = [8]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 10, }, + TimingObservation { obs_id: OID_02, interval_us: 10, }, + TimingObservation { obs_id: OID_03, interval_us: 10, }, + TimingObservation { obs_id: OID_04, interval_us: 10, }, + TimingObservation { obs_id: OID_05, interval_us: 10, }, + TimingObservation { obs_id: OID_06, interval_us: 10, }, + TimingObservation { obs_id: OID_07, interval_us: 10, }, + TimingObservation { obs_id: OID_08, interval_us: 630, }, + }; + const v3 = validate_timing_uniformity(&low_mean_high_cv); + assert(v3.code == TimingUniformityCode.LowMean); + assert(v3.got64 == 87); + + const boundary_cv: [8]TimingObservation = [8]TimingObservation{ + TimingObservation { obs_id: OID_01, interval_us: 85, }, + TimingObservation { obs_id: OID_02, interval_us: 85, }, + TimingObservation { obs_id: OID_03, interval_us: 85, }, + TimingObservation { obs_id: OID_04, interval_us: 85, }, + TimingObservation { obs_id: OID_05, interval_us: 115, }, + TimingObservation { obs_id: OID_06, interval_us: 115, }, + TimingObservation { obs_id: OID_07, interval_us: 115, }, + TimingObservation { obs_id: OID_08, interval_us: 115, }, + }; + const v4 = validate_timing_uniformity(&boundary_cv); + assert(v4.code == TimingUniformityCode.Ok); +} diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/welcome_path_secret_unmasking.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/welcome_path_secret_unmasking.t27 new file mode 100644 index 0000000000..47a722ebde --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/welcome_path_secret_unmasking.t27 @@ -0,0 +1,346 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::welcome_path_secret_unmasking; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/welcome_path_secret_unmasking.rs +// (CR-CHAT-04 - Welcome path_secret unmasking defense, Wave-30 +// Lane B, requirement R-CHAT-4; RFC 9420 12.4.3.2 and 7.6). +// +// A new member joins through a Welcome that HPKE-masks one +// path_secret per ancestor node. A forger can collapse the +// TreeKEM by delivering one secret to many nodes, substitute +// the all-zero secret, pad the path with the joiner's own leaf +// (off-leaf unmasking), or drop an ancestor the joiner needs +// and silently lose forward secrecy. The rules, in the +// original's fixed order: every secret exactly thirty-two +// bytes, the group id equal to the local one, the epoch not +// stale, no secret shared across distinct positions, the +// joiner's own leaf never sent, and every required ancestor +// delivered. +// +// The original returns Result<(), WelcomePathSecretError>. This +// port carries the verdict as a named struct (owner decision +// D5): Ok first, then the six variants in the original's +// declaration order - NonCanonicalSecretLength, +// CrossGroupWelcome, StaleEpochWelcome, DuplicatePathSecret, +// OffLeafPathSecret, MissingAncestorPathSecret - with the one +// payload field: the missing node index rides the verdict for +// rule six. +// +// The original's WelcomePacket and receiver View become the +// validator's parameters (t27 has no slice fields); the group +// ids become the fixed [17]u8 form - both fixture ids are +// seventeen bytes - compared by a scan. The Vec path +// secret becomes the fixed [32]u8 form plus its length; the +// BTreeMap and BTreeSet probes become scans. A secret repeated +// at the SAME position re-inserts cleanly, exactly as the +// original's map allows. + +// Canonical path_secret length for MLS HPKE-OKP suites. +pub const WELCOME_PATH_SECRET_LEN: usize = 32; + +// One node on the joiner's direct path. +pub struct WelcomePathSecret { + position: u32, + path_secret: [32]u8, + secret_len: usize, +} + +// Verdict codes: Ok(()) and WelcomePathSecretError's variants, in declaration order. +pub enum WelcomePathSecretCode { + Ok, + NonCanonicalSecretLength, + CrossGroupWelcome, + StaleEpochWelcome, + DuplicatePathSecret, + OffLeafPathSecret, + MissingAncestorPathSecret, +} + +// The verdict of the Welcome path-secret validation, with the +// missing node index for rule six. +pub struct WelcomePathSecretVerdict { + code: WelcomePathSecretCode, + missing: u32, +} + +// Whole-form equality over the fixed thirty-two secret bytes. +pub fn secrets_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whole-form equality over the seventeen group-id bytes. +pub fn group_ids_equal(a: [17]u8, b: [17]u8) -> bool { + for i in 0..17 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether secret i shares its bytes with an earlier secret +// under a different position - the original's BTreeMap probe, +// answered by a scan. A repeat at the same position is the +// original's clean re-insert and passes. +pub fn secret_dup_at_other_position(path_secrets: []const WelcomePathSecret, i: usize) -> bool { + for j in 0..i { + if (secrets_equal(path_secrets[j].path_secret, path_secrets[i].path_secret)) { + if (path_secrets[j].position != path_secrets[i].position) { + return true; + } + } + } + return false; +} + +// Whether any delivered secret sits at the given position - the +// original's BTreeSet membership probe, answered by a scan. +pub fn position_delivered(path_secrets: []const WelcomePathSecret, pos: u32) -> bool { + for i in 0..path_secrets.len { + if (path_secrets[i].position == pos) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate the path_secret bundle inside a Welcome +// against the joiner view. Returns Ok only when all six rules +// pass, checked in the original's fixed order: the secret +// lengths, the group id, the epoch, the cross-position +// duplicate scan, the joiner's own leaf, and the required +// ancestors. +pub fn validate_welcome_path_secrets( + group_id: [17]u8, + epoch: u64, + joiner_leaf: u32, + path_secrets: []const WelcomePathSecret, + local_group_id: [17]u8, + current_epoch: u64, + required_ancestors: []const u32, +) -> WelcomePathSecretVerdict { + for i in 0..path_secrets.len { + if (path_secrets[i].secret_len != WELCOME_PATH_SECRET_LEN) { + return WelcomePathSecretVerdict { code: WelcomePathSecretCode.NonCanonicalSecretLength, missing: 0, }; + } + } + if (!group_ids_equal(group_id, local_group_id)) { + return WelcomePathSecretVerdict { code: WelcomePathSecretCode.CrossGroupWelcome, missing: 0, }; + } + if (epoch < current_epoch) { + return WelcomePathSecretVerdict { code: WelcomePathSecretCode.StaleEpochWelcome, missing: 0, }; + } + for i in 0..path_secrets.len { + if (secret_dup_at_other_position(path_secrets, i)) { + return WelcomePathSecretVerdict { code: WelcomePathSecretCode.DuplicatePathSecret, missing: 0, }; + } + } + for i in 0..path_secrets.len { + if (path_secrets[i].position == joiner_leaf) { + return WelcomePathSecretVerdict { code: WelcomePathSecretCode.OffLeafPathSecret, missing: 0, }; + } + } + for r in 0..required_ancestors.len { + if (!position_delivered(path_secrets, required_ancestors[r])) { + return WelcomePathSecretVerdict { code: WelcomePathSecretCode.MissingAncestorPathSecret, missing: required_ancestors[r], }; + } + } + return WelcomePathSecretVerdict { code: WelcomePathSecretCode.Ok, missing: 0, }; +} + +// The group ids of the fixtures: "trinity-group-001" and its +// hostile stand-in "hostile-group-XYZ", byte by byte. +pub const GROUP_ID_LOCAL: [17]u8 = [17]u8{116, 114, 105, 110, 105, 116, 121, 45, 103, 114, 111, 117, 112, 45, 48, 48, 49,}; +pub const GROUP_ID_HOSTILE: [17]u8 = [17]u8{104, 111, 115, 116, 105, 108, 101, 45, 103, 114, 111, 117, 112, 45, 88, 89, 90,}; + +// The path secrets of the fixtures, one byte repeated thirty-two +// times - the original's canon_secret helper, entry by entry. +pub const SECRET_33: [32]u8 = [32]u8{51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,}; +pub const SECRET_77: [32]u8 = [32]u8{119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119,}; +pub const SECRET_AA: [32]u8 = [32]u8{170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,}; +pub const SECRET_ZERO: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The required ancestors of the base view: nodes 3 and 7. +pub const REQUIRED_2: [2]u32 = [2]u32{3, 7,}; + +// **WPS-01** - a short sixteen-byte path_secret rejected. +test wps_01_short_secret_rejected { + const secrets: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 16, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.NonCanonicalSecretLength); +} + +// **WPS-02** - an over-long sixty-four-byte path_secret rejected. +test wps_02_long_secret_rejected { + const secrets: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 64, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.NonCanonicalSecretLength); +} + +// **WPS-03** - a cross-group Welcome rejected. +test wps_03_cross_group_welcome_rejected { + const secrets: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_HOSTILE, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.CrossGroupWelcome); +} + +// **WPS-04** - a stale-epoch Welcome rejected: epoch four of a +// current five. +test wps_04_stale_epoch_welcome_rejected { + const secrets: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 4, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.StaleEpochWelcome); +} + +// **WPS-05** - a path_secret shared across two positions +// rejected: position seven carries position three's bytes. +test wps_05_duplicate_path_secret_rejected { + const secrets: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_33, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.DuplicatePathSecret); +} + +// **WPS-06** - the joiner's own leaf delivered: off-leaf +// unmasking rejected. +test wps_06_off_leaf_path_secret_rejected { + const secrets: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 3, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.OffLeafPathSecret); +} + +// **WPS-07** - a missing required ancestor rejected: node seven +// never delivered, its index carried back. +test wps_07_missing_required_ancestor_rejected { + const secrets: [1]WelcomePathSecret = [1]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.MissingAncestorPathSecret); + assert(v.missing == 7); +} + +// **WPS-08** - an extra optional ancestor accepted: node eleven +// rides along harmlessly. +test wps_08_extra_optional_ancestor_accepted { + const secrets: [3]WelcomePathSecret = [3]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + WelcomePathSecret { position: 11, path_secret: SECRET_AA, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.Ok); +} + +// **WPS-09** - the valid Welcome accepted: both required +// ancestors, fresh epoch, matching group. +test wps_09_valid_welcome_accepted { + const secrets: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v.code == WelcomePathSecretCode.Ok); +} + +// **WPS-10** - module green: an empty bundle against an empty +// requirement set validates clean - the original's green check +// on a live call. +test wps_10_module_green { + const secrets: [0]WelcomePathSecret = [0]WelcomePathSecret{}; + const none: [0]u32 = [0]u32{}; + const v = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &secrets, GROUP_ID_LOCAL, 5, &none); + assert(v.code == WelcomePathSecretCode.Ok); +} + +// The four helpers have direct tests (coverage: Untested 0). +test wps_helpers_answer { + assert(secrets_equal(SECRET_33, SECRET_33)); + assert(!secrets_equal(SECRET_33, SECRET_77)); + assert(group_ids_equal(GROUP_ID_LOCAL, GROUP_ID_LOCAL)); + assert(!group_ids_equal(GROUP_ID_LOCAL, GROUP_ID_HOSTILE)); + const pair: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + }; + assert(!secret_dup_at_other_position(&pair, 1)); + const cross: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_33, secret_len: 32, }, + }; + assert(!secret_dup_at_other_position(&cross, 0)); + assert(secret_dup_at_other_position(&cross, 1)); + assert(position_delivered(&cross, 3)); + assert(position_delivered(&cross, 7)); + assert(!position_delivered(&cross, 11)); +} + +// Rule order is the original's: the secret lengths before the +// group id, the group id before the epoch, the epoch before the +// duplicate scan, the duplicate scan before the off-leaf check, +// the off-leaf check before the missing-ancestor scan - and a +// secret repeated at the same position re-inserts cleanly. +test rule_order_matches_original { + const short_hostile: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 16, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v1 = validate_welcome_path_secrets(GROUP_ID_HOSTILE, 6, 0, &short_hostile, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v1.code == WelcomePathSecretCode.NonCanonicalSecretLength); + + const hostile_stale: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v2 = validate_welcome_path_secrets(GROUP_ID_HOSTILE, 4, 0, &hostile_stale, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v2.code == WelcomePathSecretCode.CrossGroupWelcome); + + const stale_dup: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_33, secret_len: 32, }, + }; + const v3 = validate_welcome_path_secrets(GROUP_ID_LOCAL, 4, 0, &stale_dup, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v3.code == WelcomePathSecretCode.StaleEpochWelcome); + + const dup_off_leaf: [2]WelcomePathSecret = [2]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_33, secret_len: 32, }, + }; + const v4 = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 3, &dup_off_leaf, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v4.code == WelcomePathSecretCode.DuplicatePathSecret); + + const off_leaf_missing: [1]WelcomePathSecret = [1]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + }; + const v5 = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 3, &off_leaf_missing, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v5.code == WelcomePathSecretCode.OffLeafPathSecret); + + const re_insert: [3]WelcomePathSecret = [3]WelcomePathSecret{ + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 3, path_secret: SECRET_33, secret_len: 32, }, + WelcomePathSecret { position: 7, path_secret: SECRET_77, secret_len: 32, }, + }; + const v6 = validate_welcome_path_secrets(GROUP_ID_LOCAL, 6, 0, &re_insert, GROUP_ID_LOCAL, 5, &REQUIRED_2); + assert(v6.code == WelcomePathSecretCode.Ok); +} diff --git a/tools/duplicate_bodies_baseline.txt b/tools/duplicate_bodies_baseline.txt index e087d2a1c0..a9415f0ca4 100644 --- a/tools/duplicate_bodies_baseline.txt +++ b/tools/duplicate_bodies_baseline.txt @@ -8,15 +8,18 @@ build_tree 28,2 magmul 22,2 smul 20,2 tmul 13 +block_hashes_equal 10 relu 10 bytes_eq 8,3 dot27 8,5 contrib 7 activate 6 +block_ids_equal 6 clamp 6 clear 6,2 quantize 6,5 relu_prime 6 +block_id_zero 5 count 5 exp_approx 5 floor 5,4 @@ -36,6 +39,7 @@ neuron4 4 str_eq 4 arena_reserve 3 assign 3 +bytes_equal 3 category 3,2 check_balanced_braces 3 concat 3,2 @@ -71,7 +75,10 @@ LinkSection_data 2 LinkSection_text 2 adaptive_temperature_by_sacred_score 2 add 2 +ads_equal 2 all_positive 2 +all_zero_bytes 2 +approx_sqrt 2 asr9 2 assert_pass 2 axi4_full_master 2 @@ -85,6 +92,7 @@ bytes32_eq 2 bytes64_eq 2 carries_src2 2 charged 2 +chi_squared_flat 2 cordic_cos 2 cordic_sin 2 cordic_x_next 2 @@ -108,6 +116,7 @@ has_at 2 has_star_inner 2 hex_digit 2 hw_place_before 2 +id_provisioned 2 id_seen_before 2,2 int_to_bytes 2 isDatabaseConfigured 2 @@ -140,6 +149,7 @@ price_tokens 2 put 2 put_lit 2 put_range 2 +read_be32 2 remove 2 runRailwayLogsCommand 2 run_die_level_with 2