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