From 8fb85af35c9acb9226638db0e105196b8abe36f7 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sat, 17 Oct 2026 05:51:15 -0300 Subject: [PATCH] spec(port): port the commit secret export collision guard (Refs #8883) The second CR-CHAT-03 ring target (W3, L-CHAT-3-csec, R-CHAT-11, RFC 9420 8.4/9): the commit-secret export 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 - as the D5 verdict (all seven original variants carry no payload, so the code is the whole verdict), the original's rule order, and all ten CSEC tests transcribed plus helper and rule-order tests. 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 upper bound), and the vector of exported pairs a slice of records holding the transcript and the secret with their lengths - the original's Vec and tuple contains become scans. The known-hash record's length field is named hash_len: a field named len anywhere in the spec breaks gen-rust's slice-length lowering (the DMPE lesson, bitten again here). Checks (conventions 0-14): t27b test 15 passed 0 failed; t27c test-report 15/15, 0 vacuous; gen-rust output compiles under rustc; coverage Untested 0; ASCII only; seal saved, all hashes MATCH; --- ...::src::commit_secret_export_collision.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/commit_secret_export_collision.t27 | 387 ++++++++++++++++++ 3 files changed, 416 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-03::src::commit_secret_export_collision.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/commit_secret_export_collision.t27 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/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index c041cbab56..ab6f9c6332 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -851,6 +851,7 @@ {"path": "specs/port/trios/crates/trios-bridge/src/main.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-01/src/key_package_expiry_guard.t27", "t27b": "pass"}, {"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-03/src/commit_secret_export_collision.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/unmerged_leaves_bound_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, 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); +}