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
//! Repository policy scrub tests.
//!
//! This file is a lightweight "policy CI" layer implemented as unit tests:
//! it scans selected source files with `include_str!` and fails when banned API
//! names, missing safety markers, or surface-contract regressions reappear.
//!
//! The goal is to catch architectural drift early (naming regressions, Ct policy
//! regressions, unsafe root exports) without introducing a separate lint tool.
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
// Shared helper for negative text assertions used by policy checks below.
fn assert_none(label: &str, haystack: &str, forbidden: &[&str]) {
for needle in forbidden {
assert!(
!haystack.contains(needle),
"{label} contains forbidden pattern: {needle}"
);
}
}
#[test]
fn legacy_pk_api_names_do_not_reappear() {
let files = [
("public_key/mod.rs", include_str!("public_key/mod.rs")),
("public_key/dsa.rs", include_str!("public_key/dsa.rs")),
("public_key/ecdsa.rs", include_str!("public_key/ecdsa.rs")),
(
"public_key/elgamal.rs",
include_str!("public_key/elgamal.rs"),
),
(
"public_key/ec_elgamal.rs",
include_str!("public_key/ec_elgamal.rs"),
),
("public_key/ecdh.rs", include_str!("public_key/ecdh.rs")),
(
"public_key/edwards_dh.rs",
include_str!("public_key/edwards_dh.rs"),
),
];
let forbidden = [
"sign_with_k(",
"verify_raw(",
"to_binary(",
"from_binary(",
"to_bytes(",
"from_bytes(",
"encrypt_with_ephemeral(",
"encrypt_point_with_k(",
];
for (label, content) in files {
assert_none(label, content, &forbidden);
}
}
#[test]
fn explicit_agreement_names_stay_in_place() {
let dh = include_str!("public_key/dh.rs");
let ecdh = include_str!("public_key/ecdh.rs");
let edwards = include_str!("public_key/edwards_dh.rs");
assert!(dh.contains("agree_element("));
assert!(ecdh.contains("agree_x_coordinate("));
assert!(edwards.contains("agree_compressed_point("));
}
#[test]
fn ct_mask_helper_stays_arithmetic_only() {
let ct = include_str!("ct.rs");
assert_none("ct.rs", ct, &["u8::from(a == b)", "wrapping_mul(u8::from("]);
assert!(ct.contains("fn constant_time_eq_mask"));
}
#[test]
fn removed_reference_generators_do_not_reappear() {
let cprng_mod = include_str!("cprng/mod.rs");
assert_none(
"cprng/mod.rs",
cprng_mod,
&["blum_blum_shub", "blum_micali"],
);
}
#[test]
fn root_exports_do_not_expose_variable_time_pk_directly() {
let lib = include_str!("lib.rs");
assert!(lib.contains("pub mod vt"));
assert_none("lib.rs", lib, &["pub use public_key::"]);
}
#[test]
fn stream_and_aead_traits_remain_in_root_surface() {
let lib = include_str!("lib.rs");
assert!(lib.contains("pub trait StreamCipher"));
assert!(lib.contains("pub trait Aead"));
assert!(lib.contains("pub use modes::{"));
assert!(lib.contains("ChaCha20Poly1305"));
}
#[test]
fn hkdf_surface_remains_exported() {
let hash_mod = include_str!("hash/mod.rs");
let lib = include_str!("lib.rs");
assert!(hash_mod.contains("pub mod hkdf;"));
assert!(lib.contains("pub use hash::hkdf::Hkdf;"));
}
#[test]
fn cipher_modules_are_classified_for_ct_policy() {
// Policy gate:
// - each public cipher module must be explicitly categorized as either
// requiring a separate Ct variant, or exempt because the primitive
// is already table-free and the fast path is constant-time by design.
// This makes "any new cipher needs a Ct path" mechanically enforced:
// adding a new module without classifying it fails CI immediately.
let ciphers_mod = include_str!("ciphers/mod.rs");
let lib = include_str!("lib.rs");
let mut public_modules = BTreeSet::<String>::new();
for line in ciphers_mod.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("pub mod ") {
let module = rest.trim_end_matches(';').trim().to_string();
public_modules.insert(module);
}
}
let required_ct_modules = BTreeSet::from([
"aes",
"camellia",
"cast128",
"des",
"grasshopper",
"magma",
"present",
"seed",
"serpent",
"sm4",
"snow3g",
"twofish",
"zuc",
]);
let ct_exempt_modules = BTreeSet::from(["chacha20", "rabbit", "salsa20", "simon", "speck"]);
let mut classified = BTreeSet::new();
classified.extend(required_ct_modules.iter().copied());
classified.extend(ct_exempt_modules.iter().copied());
for module in &public_modules {
assert!(
classified.contains(module.as_str()),
"new cipher module `{module}` is not classified for Ct policy",
);
}
for module in &required_ct_modules {
assert!(
public_modules.contains(*module),
"Ct-required module `{module}` missing from ciphers/mod.rs",
);
}
// Root export checks for Ct-required modules.
for ct_export in [
"Aes128Ct",
"Camellia128Ct",
"Cast128Ct",
"DesCt",
"GrasshopperCt",
"MagmaCt",
"Present80Ct",
"SeedCt",
"Serpent128Ct",
"Sm4Ct",
"Snow3gCt",
"Twofish128Ct",
"Zuc128Ct",
] {
assert!(
lib.contains(ct_export),
"missing required Ct export `{ct_export}` in lib.rs"
);
}
// Source-level ct implementation checks.
//
// Verifies that each Ct-required module's source contains at least one
// ct S-box indicator — a function or primitive specific to the constant-
// time path. This is stronger than the export-name check above: it
// would catch a module that exports a `*Ct` name but contains no ct
// S-box implementation at all.
//
// Limitation: this does not prove that the Ct struct *dispatches* to
// the ct path — it only proves a ct implementation exists in the file.
// The per-cipher `ct_sboxes_match_tables` and `fast_and_ct_match` tests
// provide the behavioral contract; this check catches gross structural
// omissions (e.g. a new cipher added with an empty Ct stub).
//
// The indicators are intentionally cipher-specific because ct strategies
// differ: generic ANF (`eval_byte_sbox`), generic table-scan
// (`ct_lookup_u32`), custom ANF (`sbox_ct`, `pi_ct`), or synthesized
// boolean circuit (`sbox_bool`).
let ct_indicators: &[&str] = &[
"eval_byte_sbox", // generic 8-bit ANF (Grasshopper, Camellia, SEED, SM4, SNOW 3G, ZUC)
"eval_nibble_sbox", // generic 4-bit ANF (PRESENT, Serpent)
"ct_lookup_u32", // full-table-scan 256-entry (CAST-128)
"ct_lookup_u8_16", // full-table-scan 16-entry (Twofish)
"sbox_bool", // synthesized boolean circuit (AES)
"sbox_ct", // custom ANF per-S-box (DES)
"pi_ct", // custom ANF per-S-box (Magma)
];
let ct_module_sources: &[(&str, &str)] = &[
("aes", include_str!("ciphers/aes.rs")),
("camellia", include_str!("ciphers/camellia.rs")),
("cast128", include_str!("ciphers/cast128.rs")),
("des", include_str!("ciphers/des.rs")),
("grasshopper",include_str!("ciphers/grasshopper.rs")),
("magma", include_str!("ciphers/magma.rs")),
("present", include_str!("ciphers/present.rs")),
("seed", include_str!("ciphers/seed.rs")),
("serpent", include_str!("ciphers/serpent.rs")),
("sm4", include_str!("ciphers/sm4.rs")),
("snow3g", include_str!("ciphers/snow3g.rs")),
("twofish", include_str!("ciphers/twofish.rs")),
("zuc", include_str!("ciphers/zuc.rs")),
];
for (name, src) in ct_module_sources {
let has_ct_impl = ct_indicators.iter().any(|marker| src.contains(marker));
assert!(
has_ct_impl,
"Ct-required module `{name}` contains no ct S-box indicator \
({}) — Ct struct may be an empty stub",
ct_indicators.join(", ")
);
}
}
}