1use std::collections::BTreeMap;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Scheme {
11 Ed25519,
13 Dsig,
15}
16impl Scheme {
17 pub fn name(self) -> &'static str {
19 match self {
20 Self::Ed25519 => "ed25519",
21 Self::Dsig => "dsig",
22 }
23 }
24 pub fn profile(self) -> &'static str {
26 match self {
27 Self::Ed25519 => "ED25519_SHA512_RUB2",
28 Self::Dsig => "DSIG_FPGA_W4_HARAKA_BLAKE3_V1",
29 }
30 }
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct UbftBuild {
37 pub scheme: Scheme,
39 pub nodes: u32,
41 pub window: u32,
43 pub verify_machines: u32,
45 pub dsig_slices: u32,
47 pub dsig_message_bytes: u32,
49 pub dsig_allow_profile1_ubft: bool,
51}
52impl UbftBuild {
53 pub fn validate(&self) -> Result<(), String> {
55 if !matches!(self.dsig_message_bytes, 16 | 64) {
56 return Err("dsig_message_bytes must be 16 or 64".into());
57 }
58 if self.scheme == Scheme::Dsig && self.dsig_message_bytes == 16 && !self.dsig_allow_profile1_ubft {
59 return Err("16-byte DSig uBFT requires explicit dsig_allow_profile1_ubft; full context requires 64 bytes".into());
60 }
61 if self.scheme == Scheme::Ed25519 && (self.dsig_message_bytes != 64 || self.dsig_allow_profile1_ubft) {
62 return Err("DSig message controls cannot tune Ed25519; use neutral 64 and false".into());
63 }
64 let max_nodes = if self.scheme == Scheme::Dsig { 8 } else { 16 };
65 if !(3..=max_nodes).contains(&self.nodes) {
66 return Err(format!("nodes must be 3..={max_nodes} for {} key capacity", self.scheme.name()));
67 }
68 if !(1..=8).contains(&self.window) {
69 return Err("window must be 1..=8".into());
70 }
71 if !(1..=4).contains(&self.verify_machines) {
72 return Err("verify_machines must be 1..=4".into());
73 }
74 if self.scheme == Scheme::Dsig && self.verify_machines != 1 {
75 return Err("DSig's current uBFT service has one verifier; this knob is not wired there".into());
76 }
77 if !matches!(self.dsig_slices, 2 | 4 | 8) {
78 return Err("dsig_slices must be 2, 4 or 8 (6 has no supported Merkle geometry)".into());
79 }
80 if self.scheme == Scheme::Ed25519 && self.dsig_slices != 2 {
81 return Err("dsig_slices cannot tune an Ed25519 build; leave the neutral value 2".into());
82 }
83 Ok(())
84 }
85 pub fn from_toml(text: &str) -> Result<Self, String> {
88 let doc: toml::Table = text.parse().map_err(|e| format!("invalid TOML: {e}"))?;
89 let table = doc
90 .get("rhdl_build")
91 .and_then(toml::Value::as_table)
92 .ok_or("missing [rhdl_build]; this legacy/model-only plan cannot be applied")?;
93 const KEYS: &[&str] = &[
94 "schema",
95 "scheme",
96 "profile",
97 "nodes",
98 "window",
99 "verify_machines",
100 "dsig_slices",
101 "dsig_message_bytes",
102 "dsig_allow_profile1_ubft",
103 "buildable",
104 ];
105 for key in table.keys() {
106 if !KEYS.contains(&key.as_str()) {
107 return Err(format!("unsupported build knob {key}"));
108 }
109 }
110 let number = |key: &str| -> Result<u32, String> {
111 table
112 .get(key)
113 .and_then(toml::Value::as_integer)
114 .and_then(|n| u32::try_from(n).ok())
115 .ok_or_else(|| format!("{key} must be an explicit nonnegative integer"))
116 };
117 if number("schema")? != 1 {
118 return Err("unsupported build schema".into());
119 }
120 if table.get("buildable").and_then(toml::Value::as_bool) != Some(true) {
121 return Err("plan has missing or unsupported components; buildable is not true".into());
122 }
123 let scheme = match table.get("scheme").and_then(toml::Value::as_str) {
124 Some("ed25519") => Scheme::Ed25519,
125 Some("dsig") => Scheme::Dsig,
126 _ => return Err("scheme must be ed25519 or dsig".into()),
127 };
128 if table.get("profile").and_then(toml::Value::as_str) != Some(scheme.profile()) {
129 return Err("profile/hash substitution is unsupported; select the validated scheme profile".into());
130 }
131 let build = Self {
132 scheme,
133 nodes: number("nodes")?,
134 window: number("window")?,
135 verify_machines: number("verify_machines")?,
136 dsig_slices: number("dsig_slices")?,
137 dsig_message_bytes: number("dsig_message_bytes")?,
138 dsig_allow_profile1_ubft: table
139 .get("dsig_allow_profile1_ubft")
140 .and_then(toml::Value::as_bool)
141 .ok_or("dsig_allow_profile1_ubft must be an explicit boolean")?,
142 };
143 build.validate()?;
144 Ok(build)
145 }
146 pub fn toml(&self, buildable: bool) -> String {
148 format!(
149 "\n# buildable means supported compilation, not measured card fit.\n[rhdl_build]\nschema = 1\nscheme = {:?}\nprofile = {:?}\nnodes = {}\nwindow = {}\nverify_machines = {}\ndsig_slices = {}\ndsig_message_bytes = {}\ndsig_allow_profile1_ubft = {}\nbuildable = {}\n",
150 self.scheme.name(),
151 self.scheme.profile(),
152 self.nodes,
153 self.window,
154 self.verify_machines,
155 self.dsig_slices,
156 self.dsig_message_bytes,
157 self.dsig_allow_profile1_ubft,
158 buildable && self.validate().is_ok()
159 )
160 }
161 pub fn configure_environment(
164 &self,
165 command: &mut std::process::Command,
166 inherited: impl IntoIterator<Item = std::ffi::OsString>,
167 ) -> Result<(), String> {
168 for key in inherited {
169 let name = key.to_string_lossy();
170 if name.starts_with("RHDL_UBFT_") || name.starts_with("ED25519_") || name.starts_with("DSIG_") {
171 command.env_remove(key);
172 }
173 }
174 command.envs(self.environment()?);
175 command.env("RHDL_UBFT_APPLIED_CONFIG", self.resolved_toml()?);
176 Ok(())
177 }
178 pub fn resolved_toml(&self) -> Result<String, String> {
181 let mut text = self.toml(true);
182 text.push_str("\n[cargo]\ndefault_features = false\n");
183 text.push_str(&format!("features = [{:?}]\n\n[environment]\n", self.scheme.name()));
184 for (key, value) in self.environment()? {
185 text.push_str(&format!("{key} = {value:?}\n"));
186 }
187 Ok(text)
188 }
189 pub fn environment(&self) -> Result<BTreeMap<String, String>, String> {
191 self.validate()?;
192 let mut env = BTreeMap::from([
193 ("RHDL_UBFT_SCHEME".into(), self.scheme.name().into()),
194 ("RHDL_UBFT_NODES".into(), self.nodes.to_string()),
195 ("RHDL_UBFT_WINDOW".into(), self.window.to_string()),
196 ("RHDL_UBFT_VERIFY_MACHINES".into(), self.verify_machines.to_string()),
197 ("ED25519_MULTIPLIER".into(), "DSP225".into()),
198 ("ED25519_SCALABLE_PROFILE".into(), "15,11,3,1".into()),
199 ("ED25519_VERIFY_PROFILE".into(), "2,1,1,1".into()),
200 ("ED25519_URAM_MACHINES".into(), usize::MAX.to_string()),
201 ("ED25519_LUT_MACHINES".into(), "0".into()),
202 ]);
203 if self.scheme == Scheme::Dsig {
204 env.extend([
208 ("DSIG_CHAIN".into(), "haraka".into()),
209 ("DSIG_MESSAGE_BYTES".into(), self.dsig_message_bytes.to_string()),
210 ("DSIG_SLICES".into(), self.dsig_slices.to_string()),
211 ("DSIG_VERIFY_UNITS".into(), "1".into()),
212 ("DSIG_TABLE_MASK".into(), "0x78".into()),
213 ("DSIG_SBOX_PLACEMENT".into(), "uram=0,bram=0".into()),
214 ]);
215 }
216 Ok(env)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 fn build() -> UbftBuild {
224 UbftBuild {
225 scheme: Scheme::Ed25519,
226 nodes: 3,
227 window: 3,
228 verify_machines: 1,
229 dsig_slices: 2,
230 dsig_message_bytes: 64,
231 dsig_allow_profile1_ubft: false,
232 }
233 }
234 #[test]
235 fn roundtrip_and_environment_carry_every_supported_knob() {
236 for nodes in 3..=16 {
237 for window in 1..=8 {
238 for verify_machines in 1..=4 {
239 let b = UbftBuild {
240 nodes,
241 window,
242 verify_machines,
243 ..build()
244 };
245 assert_eq!(UbftBuild::from_toml(&b.toml(true)).unwrap(), b);
246 let env = b.environment().unwrap();
247 assert_eq!(env["RHDL_UBFT_NODES"], nodes.to_string());
248 assert_eq!(env["RHDL_UBFT_WINDOW"], window.to_string());
249 assert_eq!(env["RHDL_UBFT_VERIFY_MACHINES"], verify_machines.to_string());
250 }
251 }
252 }
253 for dsig_slices in [2, 4, 8] {
254 let b = UbftBuild {
255 scheme: Scheme::Dsig,
256 nodes: 8,
257 dsig_slices,
258 ..build()
259 };
260 assert_eq!(UbftBuild::from_toml(&b.toml(true)).unwrap(), b);
261 let env = b.environment().unwrap();
262 assert_eq!(env["DSIG_SLICES"], dsig_slices.to_string());
263 assert_eq!(env["DSIG_MESSAGE_BYTES"], "64", "full context is the default");
264 }
265 }
266 #[test]
267 fn narrower_message_contract_requires_an_explicit_opt_in() {
268 let full = UbftBuild {
269 scheme: Scheme::Dsig,
270 ..build()
271 };
272 for bytes in [16, 64] {
273 let selected = UbftBuild {
274 dsig_message_bytes: bytes,
275 dsig_allow_profile1_ubft: bytes == 16,
276 ..full.clone()
277 };
278 assert_eq!(UbftBuild::from_toml(&selected.toml(true)).unwrap(), selected);
279 }
280 let good = full.toml(true);
281 for bad in [
282 good.replace("dsig_message_bytes = 64", "dsig_message_bytes = 16"),
283 good.replace("dsig_message_bytes = 64", "dsig_message_bytes = 32"),
284 good.replace("dsig_message_bytes = 64", "dsig_message_bytes = -1"),
285 good.replace("dsig_message_bytes = 64\n", ""),
286 good.replace("dsig_allow_profile1_ubft = false\n", ""),
287 good.replace("dsig_allow_profile1_ubft = false", "dsig_allow_profile1_ubft = \"false\""),
288 ] {
289 assert!(UbftBuild::from_toml(&bad).is_err(), "accepted {bad}");
290 }
291 let rejected = UbftBuild {
292 dsig_message_bytes: 16,
293 ..full
294 };
295 assert!(rejected.environment().is_err());
296 assert!(rejected.toml(true).contains("buildable = false"));
297 }
298
299 #[test]
300 fn invalid_or_unsupported_settings_fail_closed() {
301 let good = build().toml(true);
302 for text in [
303 good.replace("nodes = 3", "nodes = 17"),
304 good.replace("window = 3", "window = 0"),
305 good.replace("verify_machines = 1", "verify_machines = 5"),
306 good.replace("ED25519_SHA512_RUB2", "SHA256"),
307 good.replace("schema = 1", "schema = 2"),
308 good.replace("buildable = true", "buildable = false"),
309 format!("{good}queue_depth = 8\n"),
310 good.replace("nodes = 3\n", ""),
311 good.replace("nodes = 3", "nodes = -1"),
312 good.replace("nodes = 3", "nodes = 3.5"),
313 ] {
314 assert!(UbftBuild::from_toml(&text).is_err(), "accepted {text}");
315 }
316 assert!(UbftBuild::from_toml("[legacy]\ncores = 8\n").is_err());
317 for b in [
318 UbftBuild {
319 scheme: Scheme::Dsig,
320 nodes: 9,
321 ..build()
322 },
323 UbftBuild {
324 scheme: Scheme::Dsig,
325 verify_machines: 2,
326 ..build()
327 },
328 UbftBuild {
329 scheme: Scheme::Dsig,
330 dsig_slices: 6,
331 ..build()
332 },
333 ] {
334 assert!(b.validate().is_err());
335 }
336 }
337}
338
339#[cfg(test)]
340mod environment_contract {
341 use super::*;
342 #[test]
343 fn explicit_dsig_message_size_overrides_both_inherited_profiles() {
344 for (selected, inherited, allowed) in [(64, "16", false), (16, "64", true)] {
345 let build = UbftBuild {
346 scheme: Scheme::Dsig,
347 nodes: 3,
348 window: 2,
349 verify_machines: 1,
350 dsig_slices: 2,
351 dsig_message_bytes: selected,
352 dsig_allow_profile1_ubft: allowed,
353 };
354 let mut command = std::process::Command::new("unused");
355 command.env("DSIG_MESSAGE_BYTES", inherited);
356 build.configure_environment(&mut command, ["DSIG_MESSAGE_BYTES".into()]).unwrap();
357 let applied = command.get_envs().find(|(key, _)| *key == "DSIG_MESSAGE_BYTES").unwrap().1.unwrap();
358 assert_eq!(applied, selected.to_string().as_str());
359 assert_eq!(UbftBuild::from_toml(&build.resolved_toml().unwrap()).unwrap(), build);
360 let altered = build
361 .toml(true)
362 .replace("DSIG_FPGA_W4_HARAKA_BLAKE3_V1", "DSIG_FPGA_W4_HARAKA_BLAKE3_V2");
363 assert!(UbftBuild::from_toml(&altered).is_err());
364 }
365 }
366 #[test]
367 fn ambient_conflicts_are_removed_and_supported_values_replaced() {
368 let build = UbftBuild {
369 scheme: Scheme::Ed25519,
370 nodes: 3,
371 window: 2,
372 verify_machines: 4,
373 dsig_slices: 2,
374 dsig_message_bytes: 64,
375 dsig_allow_profile1_ubft: false,
376 };
377 let mut command = std::process::Command::new("unused");
378 command
379 .env("ED25519_SCALABLE_PROFILE", "99,99,99,99")
380 .env("DSIG_CHAIN", "sha512")
381 .env("RHDL_UBFT_SCHEME", "dsig");
382 build
383 .configure_environment(
384 &mut command,
385 ["ED25519_SCALABLE_PROFILE", "DSIG_CHAIN", "RHDL_UBFT_SCHEME", "DSIG_UNKNOWN", "PATH"].map(Into::into),
386 )
387 .unwrap();
388 let edits: BTreeMap<_, _> = command
389 .get_envs()
390 .map(|(k, v)| (k.to_string_lossy().into_owned(), v.map(|x| x.to_string_lossy().into_owned())))
391 .collect();
392 assert_eq!(edits["DSIG_CHAIN"], None);
393 assert_eq!(edits["DSIG_UNKNOWN"], None);
394 assert_eq!(edits["RHDL_UBFT_SCHEME"].as_deref(), Some("ed25519"));
395 assert_eq!(edits["ED25519_SCALABLE_PROFILE"].as_deref(), Some("15,11,3,1"));
396 assert!(!edits.contains_key("PATH"));
397 }
398}