Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions docs/reports/t27b_expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,7 @@
{"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_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/safety_number.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-07/src/blind_signature_sender_token.t27", "t27b": "pass"},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<u8> 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);
}
Loading