HAL  v4.5.0-124-g47ab54673
The Hardware Analyzer - a comprehensive reverse engineering and manipulation framework for gate-level netlists.
cipher_candidate.cpp
Go to the documentation of this file.
2 
11 #include "hal_core/netlist/net.h"
14 
15 #include <algorithm>
16 #include <bitset>
17 #include <unordered_map>
18 #include <unordered_set>
19 
20 namespace hal
21 {
22  namespace hawkeye
23  {
24  namespace
25  {
30  template<typename T>
31  std::vector<Gate*> sorted_by_id(const T& gates)
32  {
33  std::vector<Gate*> res(gates.begin(), gates.end());
34  std::sort(res.begin(), res.end(), [](const Gate* lhs, const Gate* rhs) { return lhs->get_id() < rhs->get_id(); });
35  return res;
36  }
37 
38  std::vector<u32> ids_of(const std::vector<Gate*>& gates)
39  {
40  std::vector<u32> res;
41  res.reserve(gates.size());
42  for (const auto* g : gates)
43  {
44  res.push_back(g->get_id());
45  }
46  return res;
47  }
48 
50  constexpr u32 MAX_SBOX_BITS = 8;
51 
57  constexpr u32 MAX_CONTROL_BITS = 8;
58 
59  // Identification tabulates an S-box over its state inputs and its control inputs together, so the combined
60  // table has to stay within what compute_truth_table will produce. Without this, raising one of the limits
61  // would not fail to compile but would make identify_sbox return an error for every S-box that reads many
62  // control inputs.
63  static_assert(MAX_SBOX_BITS + MAX_CONTROL_BITS <= BooleanFunction::MAX_TRUTH_TABLE_VARIABLES,
64  "an S-box tabulated over its state and control inputs must stay within the truth table limit");
65 
67  constexpr u32 MAX_SBOX_ROWS = 1 << MAX_SBOX_BITS;
68 
78  Result<std::string> lookup_sbox(const std::vector<std::vector<BooleanFunction::Value>>& rows, const u32 num_inputs, const SBoxDatabase& db)
79  {
80  if (num_inputs == 0 || num_inputs > MAX_SBOX_BITS)
81  {
82  return ERR("an S-box of " + std::to_string(num_inputs) + " input bits is not supported, expected between 1 and " + std::to_string(MAX_SBOX_BITS));
83  }
84 
85  const u32 num_rows = 1 << num_inputs;
86  if (rows.size() != num_rows)
87  {
88  return ERR("expected " + std::to_string(num_rows) + " rows for " + std::to_string(num_inputs) + " input bits, got " + std::to_string(rows.size()));
89  }
90  const u32 num_outputs = rows.front().size();
91 
92  std::vector<u64> values;
93  values.reserve(num_rows);
94  for (const auto& row : rows)
95  {
96  const auto u64_res = BooleanFunction::to_u64(row);
97  if (u64_res.is_error())
98  {
99  return ERR(u64_res.get_error());
100  }
101  values.push_back(u64_res.get());
102  }
103 
104  // More outputs than inputs means that some of them are linear combinations of the others, so reduce
105  // to a linearly independent set by Gaussian elimination over GF(2) and keep only those outputs. Every
106  // output is one vector over the rows of the table, which is why the elimination runs over the rows.
107  if (num_outputs != num_inputs)
108  {
109  std::vector<std::bitset<MAX_SBOX_ROWS>> mat(num_outputs);
110  for (u32 out = 0; out < num_outputs; out++)
111  {
112  for (u32 row = 0; row < num_rows; row++)
113  {
114  mat[out][row] = (values[row] >> out) & 1;
115  }
116  }
117 
118  for (u32 row = 0; row < num_rows; row++)
119  {
120  u32 pivot = 0;
121  for (u32 out = 0; out < num_outputs; out++)
122  {
123  if (mat[out][row])
124  {
125  pivot = out;
126  break;
127  }
128  }
129  for (u32 out = pivot + 1; out < num_outputs; out++)
130  {
131  if (mat[out][row])
132  {
133  mat[out] ^= mat[pivot];
134  }
135  }
136  }
137 
138  std::vector<u32> independent;
139  for (u32 out = 0; out < num_outputs; out++)
140  {
141  if (mat[out].any())
142  {
143  independent.push_back(out);
144  }
145  }
146 
147  if (independent.size() != num_inputs)
148  {
149  return OK(std::string());
150  }
151 
152  for (auto& value : values)
153  {
154  u64 reduced = 0;
155  for (u32 i = 0; i < independent.size(); i++)
156  {
157  reduced |= ((value >> independent.at(i)) & 1) << i;
158  }
159  value = reduced;
160  }
161  }
162 
163  std::vector<u8> sbox;
164  sbox.reserve(values.size());
165  for (const auto value : values)
166  {
167  sbox.push_back((u8)value);
168  }
169 
170  if (std::set<u8>(sbox.begin(), sbox.end()).size() != sbox.size())
171  {
172  // an S-box is a permutation, so a value occurring twice rules this one out
173  return OK(std::string());
174  }
175 
176  if (const auto lookup_res = db.lookup(sbox); lookup_res.is_ok())
177  {
178  return OK(lookup_res.get());
179  }
180  return OK(std::string());
181  }
182  } // namespace
183 
184  std::vector<Gate*> SBox::get_combinational_gates() const
185  {
186  // Walk back from the outputs to the flip-flops, staying within the component. A component split into
187  // several S-boxes is shared by all of them, so taking the component itself would hand out the logic of the
188  // other S-boxes as well.
189  const std::unordered_set<Gate*> in_component(component.begin(), component.end());
190 
191  std::unordered_set<Gate*> visited;
192  std::vector<Gate*> stack(output_gates.begin(), output_gates.end());
193  while (!stack.empty())
194  {
195  auto* current_gate = stack.back();
196  stack.pop_back();
197 
198  if (!visited.insert(current_gate).second)
199  {
200  continue;
201  }
202 
203  for (auto* pred_gate : current_gate->get_unique_predecessors())
204  {
205  if (in_component.count(pred_gate) && !pred_gate->get_type()->has_property(GateTypeProperty::ff))
206  {
207  stack.push_back(pred_gate);
208  }
209  }
210  }
211 
212  return sorted_by_id(visited);
213  }
214 
215  CipherCandidate::CipherCandidate(const std::set<Gate*>& round_reg)
216  {
217  m_in_reg = sorted_by_id(round_reg);
218  m_out_reg = m_in_reg;
219  m_in_reg_ids = ids_of(m_in_reg);
220  m_out_reg_ids = m_in_reg_ids;
221  m_size = m_in_reg.size();
222  m_netlist = m_in_reg.front()->get_netlist();
223  m_is_round_based = true;
224  }
225 
226  CipherCandidate::CipherCandidate(const std::set<Gate*>& in_reg, const std::set<Gate*>& out_reg)
227  {
228  m_in_reg = sorted_by_id(in_reg);
229  m_out_reg = sorted_by_id(out_reg);
230  m_in_reg_ids = ids_of(m_in_reg);
231  m_out_reg_ids = ids_of(m_out_reg);
232  m_size = m_out_reg.size();
233  m_netlist = m_out_reg.front()->get_netlist();
234  m_is_round_based = m_in_reg == m_out_reg;
235  }
236 
238  {
239  // larger candidates come first, as reducing a set of candidates relies on a candidate being visited before
240  // the smaller candidates that it may contain. The registers only break ties among candidates of equal size
241  // and are compared by gate ID, so that the order does not depend on where the gates are allocated.
242  if (this->m_size != rhs.m_size)
243  {
244  return this->m_size > rhs.m_size;
245  }
246  if (this->m_in_reg_ids != rhs.m_in_reg_ids)
247  {
248  return this->m_in_reg_ids < rhs.m_in_reg_ids;
249  }
250  return this->m_out_reg_ids < rhs.m_out_reg_ids;
251  }
252 
254  {
255  return this->m_size == rhs.m_size && this->m_in_reg_ids == rhs.m_in_reg_ids && this->m_out_reg_ids == rhs.m_out_reg_ids;
256  }
257 
259  {
260  return m_netlist;
261  }
262 
264  {
265  return m_size;
266  }
267 
269  {
270  return m_is_round_based;
271  }
272 
274  {
275  return m_has_round_function;
276  }
277 
278  const std::vector<Gate*>& CipherCandidate::get_input_reg() const
279  {
280  return m_in_reg;
281  }
282 
283  const std::vector<Gate*>& CipherCandidate::get_output_reg() const
284  {
285  return m_out_reg;
286  }
287 
288  const std::vector<Gate*>& CipherCandidate::get_round_logic() const
289  {
290  return m_round_logic;
291  }
292 
293  std::vector<Gate*> CipherCandidate::get_gates() const
294  {
295  std::set<Gate*> all(m_in_reg.begin(), m_in_reg.end());
296  all.insert(m_out_reg.begin(), m_out_reg.end());
297  all.insert(m_round_logic.begin(), m_round_logic.end());
298  return sorted_by_id(all);
299  }
300 
301  std::vector<SBox*> CipherCandidate::get_sboxes() const
302  {
303  std::vector<SBox*> res;
304  res.reserve(m_sboxes.size());
305  for (const auto& sbox : m_sboxes)
306  {
307  res.push_back(sbox.get());
308  }
309  return res;
310  }
311 
313  {
314  return m_graph.get();
315  }
316 
317  const std::set<Net*>& CipherCandidate::get_state_inputs() const
318  {
319  return m_state_inputs;
320  }
321 
322  const std::set<Net*>& CipherCandidate::get_control_inputs() const
323  {
324  return m_control_inputs;
325  }
326 
327  const std::set<Net*>& CipherCandidate::get_other_inputs() const
328  {
329  return m_other_inputs;
330  }
331 
332  const std::set<Net*>& CipherCandidate::get_state_outputs() const
333  {
334  return m_state_outputs;
335  }
336 
337  const std::map<Gate*, std::set<Gate*>>& CipherCandidate::get_input_ffs_of_gate() const
338  {
339  compute_gate_dependencies();
340  return m_input_ffs_of_gate;
341  }
342 
343  const std::map<u32, std::set<Gate*>>& CipherCandidate::get_longest_distance_to_gate() const
344  {
345  compute_gate_dependencies();
346  return m_longest_distance_to_gate;
347  }
348 
350  {
351  m_sboxes.clear();
352  }
353 
355  {
356  if (!m_has_round_function)
357  {
358  return ERR("round function has not been computed, call build_round_function first");
359  }
360 
361  // locating the S-boxes again would produce the same result, so hand out the ones located before rather
362  // than invalidating the pointers to them. Use clear_sboxes to locate them anew.
363  if (!m_sboxes.empty())
364  {
365  return OK(get_sboxes());
366  }
367 
368  const user_feedback::ProgressScope progress("hawkeye: locating S-boxes …");
369 
370  const std::unordered_set<Gate*> in_reg_lookup(m_in_reg.begin(), m_in_reg.end());
371  const std::unordered_set<Gate*> out_reg_lookup(m_out_reg.begin(), m_out_reg.end());
372 
373  // The candidate used to be copied into a netlist of its own, so that looking at the neighbors of a gate
374  // could not reach anything but the candidate. Working on the netlist itself, every such lookup has to be
375  // restricted to the candidate explicitly.
376  const auto candidate_gates = get_gates();
377  const std::unordered_set<Gate*> candidate_lookup(candidate_gates.begin(), candidate_gates.end());
378 
379  auto comp_res = graph_algorithm::get_connected_components(m_graph.get(), false);
380  if (comp_res.is_error())
381  {
382  return ERR(comp_res.get_error());
383  }
384 
385  for (const auto& component_vertices : comp_res.get())
386  {
387  // A state flip-flop of a round-based candidate is present twice in the graph, as the primary vertex
388  // feeding the round function and as the shadow vertex it writes back to. Both resolve to the same
389  // gate, so the role has to be read off the vertex: only the primary vertex of an input register gate
390  // is an input of this component. Deciding this by gate identity instead would count both as inputs
391  // for a round-based candidate, where the input and the output register are the very same gates.
392  std::set<Gate*> component_input_ffs;
393  std::set<Gate*> component_gates;
394  for (const u32 vertex : component_vertices)
395  {
396  const auto gate_res = m_graph->get_gate_from_vertex(vertex);
397  if (gate_res.is_error())
398  {
399  return ERR(gate_res.get_error());
400  }
401  auto* g = gate_res.get();
402  if (g == nullptr)
403  {
404  continue;
405  }
406 
407  component_gates.insert(g);
408  if (!m_graph->is_shadow_vertex(vertex) && in_reg_lookup.count(g))
409  {
410  component_input_ffs.insert(g);
411  }
412  }
413 
414  const std::vector<Gate*> component = sorted_by_id(component_gates);
415  const u32 number_input_ffs = component_input_ffs.size();
416 
417  if (number_input_ffs < 3)
418  {
419  // too small for an S-box
420  continue;
421  }
422 
423  if (number_input_ffs <= MAX_SBOX_BITS)
424  {
425  // assume a single S-box was found
426  std::set<Gate*> sbox_output_gates;
427  for (auto* cand_gate : component)
428  {
429  if (cand_gate->get_type()->has_property(GateTypeProperty::ff))
430  {
431  continue;
432  }
433 
434  // output gates are the combinational gates whose successors within this candidate all belong
435  // to the output register
436  const auto suc_gates = cand_gate->get_unique_successors();
437  if (std::none_of(suc_gates.begin(), suc_gates.end(), [&](Gate* g) { return candidate_lookup.count(g) && !out_reg_lookup.count(g); }))
438  {
439  sbox_output_gates.insert(cand_gate);
440  }
441  }
442 
443  if (sbox_output_gates.size() == number_input_ffs)
444  {
445  auto sbox = std::make_unique<SBox>();
446  sbox->component = component;
447  sbox->input_gates = sorted_by_id(component_input_ffs);
448  sbox->output_gates = sorted_by_id(sbox_output_gates);
449  m_sboxes.push_back(std::move(sbox));
450  }
451  continue;
452  }
453 
454  // A component reading more than 8 flip-flops is too wide to be a single S-box, so assume it holds
455  // several of them followed by the linear layer that mixes their outputs. Grow a subset of the round
456  // function outwards from the register one distance level at a time and watch how it falls apart into
457  // sub-components: as long as the linear layer has not mixed the S-boxes yet, every sub-component
458  // covers one S-box, and the input flip-flops of a sub-component form the input group of that S-box.
459  // the only place that needs to know what each gate depends on, so only pay for it here
460  compute_gate_dependencies();
461 
462  std::set<Gate*> current_subset = component_input_ffs;
463  std::vector<std::vector<std::set<Gate*>>> input_groupings;
464 
465  const u32 max_distance = m_longest_distance_to_gate.empty() ? 0 : m_longest_distance_to_gate.rbegin()->first;
466  for (u32 step = 1; step <= max_distance; step++)
467  {
468  const auto dist_it = m_longest_distance_to_gate.find(step);
469  if (dist_it == m_longest_distance_to_gate.end())
470  {
471  // no gate sits at this distance, so nothing further out is connected to the register either
472  break;
473  }
474  current_subset.insert(dist_it->second.begin(), dist_it->second.end());
475 
476  auto subgraph_res = graph_algorithm::get_subgraph(m_graph.get(), sorted_by_id(current_subset));
477  if (subgraph_res.is_error())
478  {
479  return ERR(subgraph_res.get_error());
480  }
481  const auto subgraph = std::move(subgraph_res.get());
482 
483  auto sub_comp_res = graph_algorithm::get_connected_components(subgraph.get(), false);
484  if (sub_comp_res.is_error())
485  {
486  return ERR(sub_comp_res.get_error());
487  }
488 
489  // determine the input groups feeding into distinct sub-circuits
490  std::set<u32> group_sizes;
491  std::vector<std::set<Gate*>> input_groups;
492  for (const auto& sub_component : sub_comp_res.get())
493  {
494  auto gates_res = subgraph->get_gates_from_vertices(sub_component);
495  if (gates_res.is_error())
496  {
497  return ERR(gates_res.get_error());
498  }
499 
500  // Only sub-components of the component at hand are of interest. Since the subset spans the
501  // whole round function, the other components are present as well, but they are disconnected
502  // from this one and hence never share an input flip-flop with it.
503  std::set<Gate*> input_group;
504  for (auto* sub_gate : gates_res.get())
505  {
506  if (component_input_ffs.count(sub_gate))
507  {
508  input_group.insert(sub_gate);
509  }
510  }
511 
512  if (input_group.empty())
513  {
514  continue;
515  }
516  group_sizes.insert(input_group.size());
517  input_groups.push_back(std::move(input_group));
518  }
519 
520  // the S-boxes of a round function are alike, so only accept a split into more than one group of
521  // equal size, each reading more than one flip-flop
522  if (group_sizes.size() == 1 && input_groups.size() > 1 && input_groups.front().size() > 1)
523  {
524  input_groupings.push_back(std::move(input_groups));
525  }
526  }
527 
528  for (const auto& input_groups : input_groupings)
529  {
530  for (const auto& input_group : input_groups)
531  {
532  // the output gates of the S-box reading this input group are the gates that depend on the
533  // whole group, on nothing outside it, and that feed something which does mix in other inputs
534  std::set<Gate*> output_group;
535  for (auto* comp_gate : component)
536  {
537  if (in_reg_lookup.count(comp_gate) || out_reg_lookup.count(comp_gate))
538  {
539  continue;
540  }
541 
542  const auto ffs_it = m_input_ffs_of_gate.find(comp_gate);
543  if (ffs_it == m_input_ffs_of_gate.end() || ffs_it->second.size() <= 1)
544  {
545  // disregard gates that depend on at most one input flip-flop
546  continue;
547  }
548 
549  // disregard gates that depend on input flip-flops outside the sub-component
550  if (!std::includes(input_group.begin(), input_group.end(), ffs_it->second.begin(), ffs_it->second.end()))
551  {
552  continue;
553  }
554 
555  // disregard gates whose successors all stay within the sub-component, as an S-box output
556  // has to reach the linear layer mixing it with the outputs of the other S-boxes
557  bool feeds_other_inputs = false;
558  for (auto* suc_gate : comp_gate->get_unique_successors())
559  {
560  const auto suc_it = m_input_ffs_of_gate.find(suc_gate);
561  if (!candidate_lookup.count(suc_gate) || suc_it == m_input_ffs_of_gate.end())
562  {
563  continue;
564  }
565  if (!std::includes(input_group.begin(), input_group.end(), suc_it->second.begin(), suc_it->second.end()))
566  {
567  feeds_other_inputs = true;
568  break;
569  }
570  }
571  if (!feeds_other_inputs)
572  {
573  continue;
574  }
575 
576  // disregard inverters behind output gates, as inverting an output does not make for
577  // another one. The removal below covers this as well, but only once the group is complete.
578  if (comp_gate->get_type()->has_property(GateTypeProperty::c_inverter))
579  {
580  auto preds = comp_gate->get_unique_predecessors();
581  preds.erase(std::remove_if(preds.begin(), preds.end(), [&](Gate* g) { return !candidate_lookup.count(g); }), preds.end());
582  std::sort(preds.begin(), preds.end());
583  if (std::includes(output_group.begin(), output_group.end(), preds.begin(), preds.end()))
584  {
585  continue;
586  }
587  }
588 
589  output_group.insert(comp_gate);
590  }
591 
592  // disregard output gates that only depend on other output gates
593  std::vector<Gate*> to_delete;
594  for (auto* out_gate : output_group)
595  {
596  auto pred_gates = out_gate->get_unique_predecessors();
597  pred_gates.erase(std::remove_if(pred_gates.begin(), pred_gates.end(), [&](Gate* g) { return !candidate_lookup.count(g); }), pred_gates.end());
598  if (std::all_of(pred_gates.begin(), pred_gates.end(), [&output_group](Gate* g) { return output_group.count(g); }))
599  {
600  to_delete.push_back(out_gate);
601  }
602  }
603  for (auto* del_gate : to_delete)
604  {
605  output_group.erase(del_gate);
606  }
607 
608  if (input_group.size() > MAX_SBOX_BITS || output_group.empty() || output_group.size() > 20)
609  {
610  continue;
611  }
612 
613  const std::vector<Gate*> inputs = sorted_by_id(input_group);
614  const std::vector<Gate*> outputs = sorted_by_id(output_group);
615 
616  auto add_sbox = [&](std::vector<Gate*> output_gates) {
617  auto sbox = std::make_unique<SBox>();
618  sbox->component = component;
619  sbox->input_gates = inputs;
620  sbox->output_gates = std::move(output_gates);
621  m_sboxes.push_back(std::move(sbox));
622  };
623 
624  // An S-box is square, but which of the gates found are the outputs is not certain, so guess:
625  // for one or two gates too many, every way of dropping the surplus becomes an S-box of its own.
626  // Anything else is taken as it is, and identification sorts out which guess was right.
627  if (outputs.size() == inputs.size() + 1)
628  {
629  for (u32 drop = 0; drop < outputs.size(); drop++)
630  {
631  std::vector<Gate*> reduced = outputs;
632  reduced.erase(reduced.begin() + drop);
633  add_sbox(std::move(reduced));
634  }
635  }
636  else if (outputs.size() == inputs.size() + 2)
637  {
638  for (u32 drop_1 = 0; drop_1 < outputs.size(); drop_1++)
639  {
640  for (u32 drop_2 = drop_1 + 1; drop_2 < outputs.size(); drop_2++)
641  {
642  std::vector<Gate*> reduced = outputs;
643  reduced.erase(reduced.begin() + drop_2);
644  reduced.erase(reduced.begin() + drop_1);
645  add_sbox(std::move(reduced));
646  }
647  }
648  }
649  else
650  {
651  add_sbox(outputs);
652  }
653  }
654  }
655  }
656 
657  log_info("hawkeye", "located {} S-boxes within the round function of the candidate.", m_sboxes.size());
658  return OK(get_sboxes());
659  }
660 
662  {
663  if (m_netlist == nullptr)
664  {
665  return ERR("candidate is empty");
666  }
667 
668  const user_feedback::ProgressScope progress("hawkeye: computing round function …");
669 
670  // the S-boxes are derived from the round function, so they do not survive it being computed anew
671  clear_sboxes();
672  m_round_logic.clear();
673  m_state_inputs.clear();
674  m_state_outputs.clear();
675  m_control_inputs.clear();
676  m_other_inputs.clear();
677  m_input_ffs_of_gate.clear();
678  m_longest_distance_to_gate.clear();
679  m_has_gate_dependencies = false;
680  m_graph.reset();
681  m_has_round_function = false;
682 
683  const std::unordered_set<Gate*> in_reg_lookup(m_in_reg.begin(), m_in_reg.end());
684  std::set<Gate*> state_logic;
685 
686  // walk backwards from every output flip-flop to the input register, collecting the combinational gates on
687  // the way. The registers are iterated in order of their gate IDs so that the traversal, and hence the
688  // shortcut below, does not depend on where the gates are allocated.
689  for (const auto* out_ff : m_out_reg)
690  {
691  auto ff_data_predecessors = out_ff->get_predecessors([](const GatePin* p, const Endpoint*) { return p->get_type() == PinType::data; });
692  if (ff_data_predecessors.size() != 1)
693  {
694  // a flip-flop can only have one predecessor at its data input
695  continue;
696  }
697 
698  const auto* pred_ep = ff_data_predecessors.at(0);
699  auto* first_comb_gate = pred_ep->get_gate();
700  if (!first_comb_gate->get_type()->has_property(GateTypeProperty::combinational))
701  {
702  continue;
703  }
704  m_state_outputs.insert(pred_ep->get_net());
705 
706  std::unordered_set<Gate*> visited;
707  std::vector<Gate*> stack = {first_comb_gate};
708  std::vector<Gate*> previous;
709  while (!stack.empty())
710  {
711  auto* current_gate = stack.back();
712 
713  // pop the stack if the gate on top has been dealt with completely
714  if (!previous.empty() && previous.back() == current_gate)
715  {
716  stack.pop_back();
717  previous.pop_back();
718  continue;
719  }
720 
721  visited.insert(current_gate);
722 
723  bool added = false;
724  for (auto* next_predecessor : current_gate->get_predecessors())
725  {
726  auto* predecessor_gate = next_predecessor->get_gate();
727  if (predecessor_gate->get_type()->has_property(GateTypeProperty::ff))
728  {
729  // reaching the input register means the current path computes part of the next state
730  if (in_reg_lookup.find(predecessor_gate) != in_reg_lookup.end())
731  {
732  m_state_inputs.insert(next_predecessor->get_net());
733  state_logic.insert(current_gate);
734  state_logic.insert(previous.begin(), previous.end());
735  }
736  }
737  else if (predecessor_gate->get_type()->has_property(GateTypeProperty::combinational))
738  {
739  if (visited.find(predecessor_gate) == visited.end())
740  {
741  stack.push_back(predecessor_gate);
742  added = true;
743  }
744  else if (state_logic.find(predecessor_gate) != state_logic.end())
745  {
746  state_logic.insert(current_gate);
747  state_logic.insert(previous.begin(), previous.end());
748  }
749  }
750  }
751 
752  if (added)
753  {
754  previous.push_back(current_gate);
755  }
756  else
757  {
758  stack.pop_back();
759  }
760  }
761  }
762 
763  m_round_logic = sorted_by_id(state_logic);
764 
765  // split the remaining inputs of the round function into control inputs, which reach most of it, and others
766  std::set<Net*> visited_nets;
767  for (auto* gate : m_round_logic)
768  {
769  for (auto* in_net : gate->get_fan_in_nets())
770  {
771  if (!visited_nets.insert(in_net).second)
772  {
773  continue;
774  }
775 
776  if (in_net->get_num_of_sources() != 1)
777  {
778  continue;
779  }
780 
781  if (m_state_inputs.find(in_net) != m_state_inputs.end())
782  {
783  continue;
784  }
785 
786  auto* src_gate = in_net->get_sources().at(0)->get_gate();
787  if (state_logic.find(src_gate) != state_logic.end())
788  {
789  continue;
790  }
791 
792  const u32 num_state_destinations = in_net->get_num_of_destinations([&state_logic](const Endpoint* ep) { return state_logic.find(ep->get_gate()) != state_logic.end(); });
793  if (num_state_destinations > m_size / 2)
794  {
795  m_control_inputs.insert(in_net);
796  }
797  else
798  {
799  m_other_inputs.insert(in_net);
800  }
801  }
802  }
803 
804  // Build the graph of the round function on the netlist itself. Only the gates of the candidate become
805  // vertices, so the graph is the closed world that the copied partial netlist used to provide. A round-based
806  // candidate reads and writes the same register, which would close a cycle through every state flip-flop and
807  // merge all S-boxes into a single connected component, so its register is split into a source and a sink
808  // vertex. A pipelined candidate needs no splitting, as neither register has both of its sides inside the
809  // graph to begin with.
810  std::set<Gate*> split_gates;
811  if (m_is_round_based)
812  {
813  split_gates.insert(m_out_reg.begin(), m_out_reg.end());
814  }
815 
816  auto graph_res = graph_algorithm::NetlistGraph::from_gates(get_gates(), split_gates);
817  if (graph_res.is_error())
818  {
819  return ERR(graph_res.get_error());
820  }
821  m_graph = std::move(graph_res.get());
822 
823  m_has_round_function = true;
824  return OK({});
825  }
826 
827  void CipherCandidate::compute_gate_dependencies() const
828  {
829  if (m_has_gate_dependencies)
830  {
831  return;
832  }
833  m_has_gate_dependencies = true;
834 
835  const user_feedback::ProgressScope progress("hawkeye: analyzing the round function …");
836 
837  const std::unordered_set<Gate*> out_reg_lookup(m_out_reg.begin(), m_out_reg.end());
838  const std::unordered_set<Gate*> round_logic_lookup(m_round_logic.begin(), m_round_logic.end());
839 
840  // Walk forwards from every input flip-flop to record which of them each gate depends on. Visiting a gate
841  // once per flip-flop is what makes this the expensive part of analyzing a candidate; visiting it once per
842  // *path* would be exponential in a cone that reconverges, which is what a wide false positive looks like.
843  for (auto* in_ff : m_in_reg)
844  {
845  std::unordered_set<Gate*> visited = {in_ff};
846  std::vector<Gate*> stack = {in_ff};
847  while (!stack.empty())
848  {
849  auto* current_gate = stack.back();
850  stack.pop_back();
851 
852  m_input_ffs_of_gate[current_gate].insert(in_ff);
853 
854  for (auto* next_successor : current_gate->get_successors())
855  {
856  auto* successor_gate = next_successor->get_gate();
857  if (successor_gate->get_type()->has_property(GateTypeProperty::ff))
858  {
859  // the register is where the round function ends, so record the dependency but stop here
860  if (out_reg_lookup.find(successor_gate) != out_reg_lookup.end())
861  {
862  m_input_ffs_of_gate[successor_gate].insert(in_ff);
863  }
864  }
865  else if (round_logic_lookup.find(successor_gate) != round_logic_lookup.end())
866  {
867  if (visited.insert(successor_gate).second)
868  {
869  stack.push_back(successor_gate);
870  }
871  }
872  }
873  }
874  }
875 
876  // The distance of a gate is the length of the longest path reaching it from any input flip-flop. Enumerating
877  // the paths to find the longest one is what the reachability walk above must not do, so relax the distances
878  // along a topological order of the round function instead, which visits every edge exactly once. The round
879  // function is acyclic: it runs from the register to the register, and the register is not part of it.
880  std::unordered_map<Gate*, u32> in_degree;
881  in_degree.reserve(m_round_logic.size());
882  for (auto* gate : m_round_logic)
883  {
884  in_degree[gate];
885  }
886  for (auto* gate : m_round_logic)
887  {
888  for (auto* next_successor : gate->get_successors())
889  {
890  auto* successor_gate = next_successor->get_gate();
891  if (round_logic_lookup.find(successor_gate) != round_logic_lookup.end())
892  {
893  in_degree[successor_gate]++;
894  }
895  }
896  }
897 
898  // a gate driven by the register directly sits at distance one
899  std::unordered_map<Gate*, u32> distance;
900  distance.reserve(m_round_logic.size());
901  for (auto* in_ff : m_in_reg)
902  {
903  for (auto* next_successor : in_ff->get_successors())
904  {
905  auto* successor_gate = next_successor->get_gate();
906  if (round_logic_lookup.find(successor_gate) != round_logic_lookup.end())
907  {
908  distance[successor_gate] = std::max(distance[successor_gate], u32(1));
909  }
910  }
911  }
912 
913  std::vector<Gate*> ordered;
914  ordered.reserve(m_round_logic.size());
915  for (auto* gate : m_round_logic)
916  {
917  if (in_degree[gate] == 0)
918  {
919  ordered.push_back(gate);
920  }
921  }
922  for (u32 i = 0; i < ordered.size(); i++)
923  {
924  auto* gate = ordered.at(i);
925  for (auto* next_successor : gate->get_successors())
926  {
927  auto* successor_gate = next_successor->get_gate();
928  if (round_logic_lookup.find(successor_gate) == round_logic_lookup.end())
929  {
930  continue;
931  }
932 
933  distance[successor_gate] = std::max(distance[successor_gate], distance[gate] + 1);
934  if (--in_degree[successor_gate] == 0)
935  {
936  ordered.push_back(successor_gate);
937  }
938  }
939  }
940 
941  if (ordered.size() != m_round_logic.size())
942  {
943  // a combinational loop would leave gates unordered, and their distance short rather than wrong
944  log_warning("hawkeye", "the round function of the candidate is not acyclic, {} of its {} gates were not reached in topological order.", m_round_logic.size() - ordered.size(), m_round_logic.size());
945  }
946 
947  for (const auto& [gate, gate_distance] : distance)
948  {
949  if (gate_distance != 0)
950  {
951  m_longest_distance_to_gate[gate_distance].insert(gate);
952  }
953  }
954  }
955 
957  {
958  // The search produces one S-box per guess at which of the surplus gates are the outputs, so the S-boxes
959  // reading the same input flip-flops are variants of one and the same S-box. Identifying one of them
960  // answers the question for all of them, which is what keeps this affordable.
961  std::map<std::vector<u32>, std::vector<SBox*>> variants_by_input;
962  for (const auto& sbox : m_sboxes)
963  {
964  sbox->status = SBoxStatus::unidentified;
965  sbox->identified_as.clear();
966  variants_by_input[ids_of(sbox->input_gates)].push_back(sbox.get());
967  }
968 
969  // The narrower S-boxes first, and by gate ID within one width. The wide groups tend to be several real
970  // S-boxes merged through the surrounding logic: they rarely match anything, and looking their variants up
971  // is the most expensive part of identification, as the canonical form search behind the lookup degenerates
972  // on such glued-together tables. Trying the narrow groups first therefore lands the real matches before
973  // any time is spent on the merged ones.
974  std::vector<const std::vector<SBox*>*> groups;
975  groups.reserve(variants_by_input.size());
976  for (const auto& [_, variants] : variants_by_input)
977  {
978  groups.push_back(&variants);
979  }
980  std::stable_sort(groups.begin(), groups.end(), [](const auto* lhs, const auto* rhs) { return lhs->front()->input_gates.size() < rhs->front()->input_gates.size(); });
981 
982  const user_feedback::ProgressScope progress("hawkeye: identifying S-boxes …");
983 
984  u32 num_identified = 0;
985  for (const auto* group : groups)
986  {
987  for (u32 i = 0; i < group->size(); i++)
988  {
989  auto* variant = group->at(i);
990  const auto name_res = identify_sbox(variant, db);
991  if (name_res.is_error())
992  {
993  return ERR(name_res.get_error());
994  }
995 
996  if (name_res.get().empty())
997  {
998  // this guess at the outputs was a wrong one, so try the next variant
999  continue;
1000  }
1001 
1002  variant->identified_as = name_res.get();
1003  variant->status = SBoxStatus::identified;
1004  num_identified++;
1005 
1006  for (u32 j = i + 1; j < group->size(); j++)
1007  {
1008  group->at(j)->status = SBoxStatus::superseded;
1009  }
1010  break;
1011  }
1012  }
1013 
1014  log_info("hawkeye", "identified {} of the {} S-boxes of the candidate.", num_identified, m_sboxes.size());
1015  return OK(num_identified);
1016  }
1017 
1019  {
1020  if (sbox == nullptr)
1021  {
1022  return ERR("S-box is a nullptr");
1023  }
1024 
1025  if (!m_has_round_function)
1026  {
1027  return ERR("round function has not been computed, call build_round_function first");
1028  }
1029 
1030  if (sbox->input_gates.empty() || sbox->output_gates.empty())
1031  {
1032  return ERR("S-box has no input or no output gates");
1033  }
1034 
1035  // The S-box computes its outputs from its input flip-flops, so its combinational gates are the subgraph
1036  // that the output functions are taken over. Leaving the flip-flops out of it makes them the inputs of
1037  // those functions instead of being traversed through.
1038  const std::vector<Gate*> subgraph_gates = sbox->get_combinational_gates();
1039 
1040  const auto snd = SubgraphNetlistDecorator(*m_netlist);
1041  std::map<std::pair<u32, const GatePin*>, BooleanFunction> cache;
1042 
1043  std::vector<BooleanFunction> bfs;
1044  std::set<Net*> all_inputs;
1045  for (const auto* out_gate : sbox->output_gates)
1046  {
1047  const auto& fan_out_nets = out_gate->get_fan_out_nets();
1048  if (fan_out_nets.size() != 1)
1049  {
1050  return ERR("gate '" + out_gate->get_name() + "' with ID " + std::to_string(out_gate->get_id())
1051  + " has none or multiple fan-out nets, which is currently not supported");
1052  }
1053 
1054  auto bf_res = snd.get_subgraph_function(subgraph_gates, fan_out_nets.front(), cache);
1055  if (bf_res.is_error())
1056  {
1057  return ERR(bf_res.get_error());
1058  }
1059  bfs.push_back(bf_res.get());
1060 
1061  // gather the nets that the component actually reads, which can be fewer than it is connected to
1062  for (const auto& var : bfs.back().get_variable_names())
1063  {
1064  const auto net_res = BooleanFunctionNetDecorator::get_net_from(m_netlist, var);
1065  if (net_res.is_error())
1066  {
1067  return ERR(net_res.get_error());
1068  }
1069  all_inputs.insert(net_res.get());
1070  }
1071  }
1072 
1073  // split the inputs read by this S-box the same way the round function as a whole was split
1074  std::set<Net*> state_inputs, control_inputs, other_inputs;
1075  std::set_intersection(all_inputs.begin(), all_inputs.end(), m_state_inputs.begin(), m_state_inputs.end(), std::inserter(state_inputs, state_inputs.begin()));
1076  std::set_intersection(all_inputs.begin(), all_inputs.end(), m_control_inputs.begin(), m_control_inputs.end(), std::inserter(control_inputs, control_inputs.begin()));
1077  std::set_intersection(all_inputs.begin(), all_inputs.end(), m_other_inputs.begin(), m_other_inputs.end(), std::inserter(other_inputs, other_inputs.begin()));
1078 
1079  if (state_inputs.empty() || state_inputs.size() > MAX_SBOX_BITS)
1080  {
1081  log_info("hawkeye", "skipping an S-box that reads {} state inputs, which is not a supported S-box width.", state_inputs.size());
1082  return OK(std::string());
1083  }
1084 
1085  if (control_inputs.size() > MAX_CONTROL_BITS)
1086  {
1087  log_info("hawkeye", "skipping an S-box that reads {} control inputs, which is too many to try every assignment of.", control_inputs.size());
1088  return OK(std::string());
1089  }
1090 
1091  // hold everything that is neither state nor control at '0'
1092  const auto bf_zero = BooleanFunction::Const(0, 1);
1093  for (auto& bf : bfs)
1094  {
1095  for (const auto* other_in : other_inputs)
1096  {
1097  auto sub_res = bf.substitute(BooleanFunctionNetDecorator(*other_in).get_boolean_variable_name(), bf_zero);
1098  if (sub_res.is_error())
1099  {
1100  return ERR(sub_res.get_error());
1101  }
1102  bf = sub_res.get();
1103  }
1104  }
1105 
1106  // Tabulate every output over the state and the control inputs together, so that an assignment of the
1107  // control inputs becomes a slice of that one table instead of a table of its own. Ordering the state
1108  // inputs first makes the rows of one assignment contiguous: the state inputs occupy the low bits of a row
1109  // index and the control inputs the high ones, so assignment i covers the rows i << |state| to
1110  // (i + 1) << |state|. The static_assert above keeps the combined table within what `compute_truth_table`
1111  // handles.
1112  std::vector<std::string> variable_names;
1113  for (const auto* n : state_inputs)
1114  {
1115  variable_names.push_back(BooleanFunctionNetDecorator(*n).get_boolean_variable_name());
1116  }
1117  for (const auto* n : control_inputs)
1118  {
1119  variable_names.push_back(BooleanFunctionNetDecorator(*n).get_boolean_variable_name());
1120  }
1121 
1122  std::vector<std::vector<BooleanFunction::Value>> tables;
1123  tables.reserve(bfs.size());
1124  for (const auto& bf : bfs)
1125  {
1126  auto tt_res = bf.compute_truth_table(variable_names);
1127  if (tt_res.is_error())
1128  {
1129  return ERR(tt_res.get_error());
1130  }
1131  tables.push_back(std::move(tt_res.get().front()));
1132  }
1133 
1134  // The round function computes the S-box for one assignment of the control inputs and something else for
1135  // the others, and which one that is is not known in advance, so try them all.
1136  const u32 num_state_rows = 1 << state_inputs.size();
1137  for (u32 assignment = 0; assignment < (1u << control_inputs.size()); assignment++)
1138  {
1139  std::vector<std::vector<BooleanFunction::Value>> rows(num_state_rows, std::vector<BooleanFunction::Value>(tables.size()));
1140  for (u32 out = 0; out < tables.size(); out++)
1141  {
1142  const auto& table = tables.at(out);
1143  for (u32 row = 0; row < num_state_rows; row++)
1144  {
1145  rows[row][out] = table.at((u64(assignment) << state_inputs.size()) + row);
1146  }
1147  }
1148 
1149  auto name_res = lookup_sbox(rows, state_inputs.size(), db);
1150  if (name_res.is_error())
1151  {
1152  return ERR(name_res.get_error());
1153  }
1154  if (!name_res.get().empty())
1155  {
1156  return OK(name_res.get());
1157  }
1158  }
1159 
1160  return OK(std::string());
1161  }
1162 
1163  Result<std::string> CipherCandidate::identify_sbox(const std::vector<BooleanFunction>& output_functions, const SBoxDatabase& db)
1164  {
1165  if (output_functions.empty())
1166  {
1167  return ERR("no output functions provided");
1168  }
1169 
1170  std::set<std::string> variables;
1171  for (const auto& bf : output_functions)
1172  {
1173  const auto bf_variables = bf.get_variable_names();
1174  variables.insert(bf_variables.begin(), bf_variables.end());
1175  }
1176 
1177  if (variables.empty() || variables.size() > MAX_SBOX_BITS)
1178  {
1179  return ERR("the output functions read " + std::to_string(variables.size()) + " variables, but an S-box reads between 1 and " + std::to_string(MAX_SBOX_BITS));
1180  }
1181 
1182  const std::vector<std::string> variable_names(variables.begin(), variables.end());
1183 
1184  std::vector<std::vector<BooleanFunction::Value>> tables;
1185  tables.reserve(output_functions.size());
1186  for (const auto& bf : output_functions)
1187  {
1188  auto tt_res = bf.compute_truth_table(variable_names);
1189  if (tt_res.is_error())
1190  {
1191  return ERR(tt_res.get_error());
1192  }
1193  tables.push_back(std::move(tt_res.get().front()));
1194  }
1195 
1196  std::vector<std::vector<BooleanFunction::Value>> rows(1 << variable_names.size(), std::vector<BooleanFunction::Value>(tables.size()));
1197  for (u32 out = 0; out < tables.size(); out++)
1198  {
1199  for (u32 row = 0; row < rows.size(); row++)
1200  {
1201  rows[row][out] = tables.at(out).at(row);
1202  }
1203  }
1204 
1205  return lookup_sbox(rows, variable_names.size(), db);
1206  }
1207 
1209  {
1210  if (m_netlist == nullptr)
1211  {
1212  return ERR("candidate is empty");
1213  }
1214 
1215  auto* candidate_module = m_netlist->create_module("cipher_candidate", m_netlist->get_top_module(), get_gates());
1216  if (candidate_module == nullptr)
1217  {
1218  return ERR("could not create a module for the candidate");
1219  }
1220 
1221  if (m_is_round_based)
1222  {
1223  if (m_netlist->create_module("state_register", candidate_module, m_in_reg) == nullptr)
1224  {
1225  return ERR("could not create a module for the state register of the candidate");
1226  }
1227  }
1228  else
1229  {
1230  if (m_netlist->create_module("input_register", candidate_module, m_in_reg) == nullptr)
1231  {
1232  return ERR("could not create a module for the input register of the candidate");
1233  }
1234  if (m_netlist->create_module("output_register", candidate_module, m_out_reg) == nullptr)
1235  {
1236  return ERR("could not create a module for the output register of the candidate");
1237  }
1238  }
1239 
1240  // A gate belongs to exactly one module, so two S-boxes sharing a gate cannot both become one. That the
1241  // S-boxes located overlap is expected, as the search guesses at their outputs, but by the time they have
1242  // been identified an overlap means that two guesses were both taken for real.
1243  std::unordered_set<Gate*> already_in_a_module;
1244  u32 num_sboxes = 0;
1245  for (const auto& sbox : m_sboxes)
1246  {
1247  if (sbox->status != SBoxStatus::identified)
1248  {
1249  continue;
1250  }
1251 
1252  const auto sbox_gates = sbox->get_combinational_gates();
1253  if (std::any_of(sbox_gates.begin(), sbox_gates.end(), [&already_in_a_module](Gate* g) { return already_in_a_module.count(g); }))
1254  {
1255  log_info("hawkeye", "skipping S-box '{}' reading flip-flop {}, as it overlaps an S-box that was already turned into a module.", sbox->identified_as, sbox->input_gates.front()->get_id());
1256  continue;
1257  }
1258 
1259  if (m_netlist->create_module(sbox->identified_as + "_" + std::to_string(num_sboxes), candidate_module, sbox_gates) == nullptr)
1260  {
1261  return ERR("could not create a module for S-box '" + sbox->identified_as + "' of the candidate");
1262  }
1263 
1264  already_in_a_module.insert(sbox_gates.begin(), sbox_gates.end());
1265  num_sboxes++;
1266  }
1267 
1268  log_info("hawkeye", "created a module for the candidate holding {} S-box modules.", num_sboxes);
1269  return OK(candidate_module);
1270  }
1271  } // namespace hawkeye
1272 } // namespace hal
std::set< u32 > out_reg
std::set< u32 > in_reg
u32 size
This file contains the class that holds all information on a candidate for a symmetric cryptographic ...
PinType get_type() const
Definition: base_pin.h:150
static constexpr u32 MAX_TRUTH_TABLE_VARIABLES
static BooleanFunction Const(const BooleanFunction::Value &value)
static Result< u64 > to_u64(const std::vector< BooleanFunction::Value > &value)
static Result< Net * > get_net_from(const Netlist *netlist, const BooleanFunction &var)
Gate * get_gate() const
Definition: endpoint.cpp:23
Definition: gate.h:58
Module * get_top_module() const
Definition: netlist.cpp:610
Module * create_module(const u32 module_id, const std::string &name, Module *parent, const std::vector< Gate * > &gates={})
Definition: netlist.cpp:589
A directed graph corresponding to a netlist.
Definition: netlist_graph.h:60
static Result< std::unique_ptr< NetlistGraph > > from_gates(const std::vector< Gate * > &gates, const std::set< Gate * > &split_gates={}, const std::function< bool(const Net *)> &filter=nullptr)
Create a directed graph from a subset of the gates of a netlist.
A candidate for a symmetric cryptographic implementation within a netlist.
const std::map< u32, std::set< Gate * > > & get_longest_distance_to_gate() const
Get a map from a distance to all gates reachable within at most that distance from any input flip-flo...
std::vector< Gate * > get_gates() const
Get all gates of the candidate, i.e., its registers together with its round function,...
const std::set< Net * > & get_control_inputs() const
Get the control inputs of the round function.
std::vector< SBox * > get_sboxes() const
Get the S-boxes located within the round function of the candidate.
Result< u32 > identify_sboxes(const SBoxDatabase &db)
Try to identify all S-boxes of the candidate by matching them against a database of known S-boxes.
graph_algorithm::NetlistGraph * get_graph() const
Get the graph of the round function, in which the gates of the state register are represented by a pr...
Result< std::vector< SBox * > > locate_sboxes()
Try to locate S-boxes within the round function of the candidate.
void clear_sboxes()
Discard the S-boxes located so far.
const std::set< Net * > & get_state_outputs() const
Get the state outputs of the round function.
const std::vector< Gate * > & get_input_reg() const
Get the input register of the candidate, ordered by gate ID.
const std::vector< Gate * > & get_round_logic() const
Get the combinational logic computing the next state, ordered by gate ID.
Result< Module * > create_modules()
Write the candidate back into the netlist as a module hierarchy.
Result< std::monostate > build_round_function()
Determine the round function of the candidate, i.e., the combinational logic computing the next state...
const std::set< Net * > & get_state_inputs() const
Get the state inputs of the round function.
bool operator==(const CipherCandidate &rhs) const
Check whether two candidates have the same size and the same registers.
Netlist * get_netlist() const
Get the netlist that the candidate belongs to.
const std::set< Net * > & get_other_inputs() const
Get the remaining inputs of the round function.
bool is_round_based() const
Check whether the candidate is round-based, i.e., whether its input and output register are the same.
Result< std::string > identify_sbox(const SBox *sbox, const SBoxDatabase &db) const
Try to identify a single S-box of this candidate by matching it against a database of known S-boxes u...
u32 get_size() const
Get the size of the candidate, i.e., the width of its state register.
bool operator<(const CipherCandidate &rhs) const
Compare two candidates.
const std::vector< Gate * > & get_output_reg() const
Get the output register of the candidate, ordered by gate ID. Equal to the input register for a round...
bool has_round_function() const
Check whether the round function of the candidate has been computed, see build_round_function.
const std::map< Gate *, std::set< Gate * > > & get_input_ffs_of_gate() const
Get a map from each gate of the round function to the input flip-flops it depends on.
Database of known S-boxes.
Definition: sbox_database.h:50
This file contains functions related to graph components.
uint64_t u64
Definition: defines.h:42
uint32_t u32
Definition: defines.h:41
uint8_t u8
Definition: defines.h:39
#define log_info(channel,...)
Definition: log.h:70
#define log_warning(channel,...)
Definition: log.h:76
#define ERR(message)
Definition: result.h:60
#define OK(...)
Definition: result.h:56
Result< std::vector< std::vector< u32 > > > get_connected_components(const NetlistGraph *graph, bool strong, u32 min_size=0)
Compute the (strongly) connected components of the specified graph.
Definition: components.cpp:11
Result< std::unique_ptr< NetlistGraph > > get_subgraph(const NetlistGraph *graph, const std::vector< Gate * > &subgraph_gates)
Compute the subgraph induced by the specified gates, including all edges between the corresponding ve...
Definition: subgraph.cpp:10
Definition: defines.h:45
bool ordered
This file contains the class that holds a netlist graph.
An S-box located within the round function of a CipherCandidate.
std::vector< Gate * > output_gates
The output gates of the S-box, ordered by gate ID. Usually combinational gates feeding the linear lay...
std::vector< Gate * > component
The gates of the connected component that the S-box was located in, including its input flip-flops.
std::vector< Gate * > input_gates
The input flip-flops of the S-box, ordered by gate ID.
std::vector< Gate * > get_combinational_gates() const
Get the combinational gates computing the outputs of the S-box from its input flip-flops.
This file contains functions related to subgraphs.