diff --git a/plugins/dataflow_analysis/src/pre_processing/pre_processing.cpp b/plugins/dataflow_analysis/src/pre_processing/pre_processing.cpp index 0a1daa8e7dc..6f05eee5f8c 100644 --- a/plugins/dataflow_analysis/src/pre_processing/pre_processing.cpp +++ b/plugins/dataflow_analysis/src/pre_processing/pre_processing.cpp @@ -107,6 +107,48 @@ namespace hal // find successors std::unordered_map, std::unordered_set>> suc_cache; std::unordered_map> pred_cache; + + // The caches are shared by the traversals of all target gates, so an entry is trusted to be + // complete. A traversal does not push a net that it has visited before, though, so whatever such + // a net leads to has to be handed to the nets of the current path explicitly. Otherwise, the + // entries of these nets miss it, and so does every later traversal that finds them in the cache. + // A visited net that is not on the current path has been explored completely; one on the path + // closes a combinational loop and is still being explored, so its entry is not complete yet. + const auto on_path = [](const Net* net, const Net* current, const std::vector& previous) { + return net == current || std::find(previous.cbegin(), previous.cend(), net) != previous.cend(); + }; + const auto pass_on_successors = [&suc_cache, &on_path](const Net* visited_net, const Net* current, const std::vector& previous) { + const auto it = suc_cache.find(visited_net); + if (it == suc_cache.end() || on_path(visited_net, current, previous)) + { + return; + } + // references to the elements of an unordered_map stay valid when inserting rehashes it + const auto& [cached_gates, cached_net_groups] = it->second; + for (const Net* n : previous) + { + auto& entry = suc_cache[n]; + std::get<0>(entry).insert(cached_gates.cbegin(), cached_gates.cend()); + std::get<1>(entry).insert(cached_net_groups.cbegin(), cached_net_groups.cend()); + } + auto& entry = suc_cache[current]; + std::get<0>(entry).insert(cached_gates.cbegin(), cached_gates.cend()); + std::get<1>(entry).insert(cached_net_groups.cbegin(), cached_net_groups.cend()); + }; + const auto pass_on_predecessors = [&pred_cache, &on_path](const Net* visited_net, const Net* current, const std::vector& previous) { + const auto it = pred_cache.find(visited_net); + if (it == pred_cache.end() || on_path(visited_net, current, previous)) + { + return; + } + const auto& cached_net_groups = it->second; + for (const Net* n : previous) + { + pred_cache[n].insert(cached_net_groups.cbegin(), cached_net_groups.cend()); + } + pred_cache[current].insert(cached_net_groups.cbegin(), cached_net_groups.cend()); + }; + for (const auto& gate : netlist_abstr.target_gates) { cnt++; @@ -203,6 +245,10 @@ namespace hal stack.push_back(n); added = true; } + else + { + pass_on_successors(n, current, previous); + } } } } @@ -279,6 +325,10 @@ namespace hal stack.push_back(n); added = true; } + else + { + pass_on_predecessors(n, current, previous); + } } } } diff --git a/plugins/hawkeye/src/cipher_candidate.cpp b/plugins/hawkeye/src/cipher_candidate.cpp index 86a522268cd..6d374630a85 100644 --- a/plugins/hawkeye/src/cipher_candidate.cpp +++ b/plugins/hawkeye/src/cipher_candidate.cpp @@ -50,6 +50,12 @@ namespace hal /** An S-box is looked up as a vector of `u8`, so it reads and writes at most 8 bits. */ constexpr u32 MAX_SBOX_BITS = 8; + /** + * An S-box reads at least 3 bits. Fewer make for no useful S-box, and synthesis often starts an S-box with + * 2-to-4 predecoders on pairs of its input bits, which would otherwise pass for many small S-boxes. + */ + constexpr u32 MIN_SBOX_BITS = 3; + /** * The most control inputs an S-box may read for identification to try every assignment of them. The * assignment loop is exponential in this, so every additional bit doubles what an S-box that matches @@ -72,8 +78,8 @@ namespace hal * the S-box that it describes and look that up in the database. * * Yields an empty name if the rows do not describe a usable S-box at all, that is, if there are more - * outputs than inputs and they cannot be reduced to as many linearly independent ones as there are - * inputs, or if the S-box is not bijective. Neither is an error: it only means that this guess at the + * outputs than inputs and they cannot be reduced to as many ones that are independent up to constants as + * there are inputs, or if the S-box is not bijective. Neither is an error: it only means that this guess at the * outputs of an S-box was a wrong one. */ Result lookup_sbox(const std::vector>& rows, const u32 num_inputs, const SBoxDatabase& db) @@ -102,44 +108,54 @@ namespace hal values.push_back(u64_res.get()); } - // More outputs than inputs means that some of them are linear combinations of the others, so reduce - // to a linearly independent set by Gaussian elimination over GF(2) and keep only those outputs. Every - // output is one vector over the rows of the table, which is why the elimination runs over the rows. + // More outputs than inputs means that some of them depend on the others, so keep only an independent + // subset of them. Every output is one vector over the rows of the table. An output may also be the + // complement of a combination of others, e.g., an XNOR, an inverter, or a key addition whose key bit + // identify_sbox holds at 0, so the outputs have to be independent modulo the constant function, not + // just linearly: the constant goes into the basis first. The database does not care about a constant + // XORed onto the outputs, so the outputs that are kept stay as they are. if (num_outputs != num_inputs) { - std::vector> mat(num_outputs); - for (u32 out = 0; out < num_outputs; out++) - { - for (u32 row = 0; row < num_rows; row++) + // The basis is kept in echelon form: no basis vector has a bit set at the pivot row of an earlier + // one, so a single pass over the basis reduces a vector completely. + std::vector> basis; + std::vector pivots; + const auto add_if_independent = [&basis, &pivots, num_rows](std::bitset vec) { + for (u32 i = 0; i < basis.size(); i++) { - mat[out][row] = (values[row] >> out) & 1; - } - } - - for (u32 row = 0; row < num_rows; row++) - { - u32 pivot = 0; - for (u32 out = 0; out < num_outputs; out++) - { - if (mat[out][row]) + if (vec[pivots[i]]) { - pivot = out; - break; + vec ^= basis[i]; } } - for (u32 out = pivot + 1; out < num_outputs; out++) + for (u32 row = 0; row < num_rows; row++) { - if (mat[out][row]) + if (vec[row]) { - mat[out] ^= mat[pivot]; + basis.push_back(vec); + pivots.push_back(row); + return true; } } + return false; + }; + + std::bitset constant; + for (u32 row = 0; row < num_rows; row++) + { + constant[row] = 1; } + add_if_independent(constant); std::vector independent; for (u32 out = 0; out < num_outputs; out++) { - if (mat[out].any()) + std::bitset vec; + for (u32 row = 0; row < num_rows; row++) + { + vec[row] = (values[row] >> out) & 1; + } + if (add_if_independent(vec)) { independent.push_back(out); } @@ -415,7 +431,7 @@ namespace hal const std::vector component = sorted_by_id(component_gates); const u32 number_input_ffs = component_input_ffs.size(); - if (number_input_ffs < 3) + if (number_input_ffs < MIN_SBOX_BITS) { // too small for an S-box continue; @@ -519,8 +535,8 @@ namespace hal } // the S-boxes of a round function are alike, so only accept a split into more than one group of - // equal size, each reading more than one flip-flop - if (group_sizes.size() == 1 && input_groups.size() > 1 && input_groups.front().size() > 1) + // equal size, each large enough for an S-box + if (group_sizes.size() == 1 && input_groups.size() > 1 && input_groups.front().size() >= MIN_SBOX_BITS) { input_groupings.push_back(std::move(input_groups)); } diff --git a/plugins/hawkeye/src/sbox_database.cpp b/plugins/hawkeye/src/sbox_database.cpp index a29005f2f26..833e10dd4b8 100644 --- a/plugins/hawkeye/src/sbox_database.cpp +++ b/plugins/hawkeye/src/sbox_database.cpp @@ -6,6 +6,7 @@ #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" +#include #include #include #include @@ -296,6 +297,32 @@ namespace hal constexpr u64 LINEAR_REPRESENTATIVE_BUDGET = 250000; std::vector compute_linear_representative_bounded(const std::vector& sbox, u64& budget); + + /** + * How often every value occurs in the difference distribution table of an S-box, over all nonzero input + * differences: entry c counts the pairs of input and output difference that c inputs map onto each other. + * Linear and affine equivalence and XORing constants onto inputs or outputs only permute the entries of the + * table, so two S-boxes that the database considers equal have the same spectrum (but not vice versa). + */ + std::vector differential_spectrum(const std::vector& sbox) + { + const u32 len = sbox.size(); + std::vector spectrum(len + 1, 0); + std::vector row(256); + for (u32 a = 1; a < len; a++) + { + std::fill(row.begin(), row.end(), 0); + for (u32 x = 0; x < len; x++) + { + row[sbox[x] ^ sbox[x ^ a]]++; + } + for (const u32 count : row) + { + spectrum[count]++; + } + } + return spectrum; + } } // namespace SBoxDatabase::SBoxDatabase(const std::map>& sboxes) @@ -337,6 +364,13 @@ namespace hal auto lin_rep = compute_linear_representative_bounded(sbox_alpha, budget); if (budget == 0) { + if (sbox_alpha.front() == 0) + { + // A table that maps 0 to 0 takes a search of its own, which can exhaust the budget for this + // one alpha only, e.g., for AES and alpha = 0x63 (inversion in GF(2^8)). Leave the entry out: + // lookup skips the corresponding beta as well and finds such an S-box through the others. + continue; + } // storing a representative from a truncated search would silently break every lookup against this // entry, and a real S-box is never degenerate enough to exhaust the search in the first place return ERR("cannot add S-box '" + name + "' to the database: the canonical form search was abandoned, the S-box is too close to linear"); @@ -483,6 +517,16 @@ namespace hal { return ERR("no S-box of matching bit-size of " + std::to_string(bit_size) + " bits contained in database"); } + const auto& matching_size_data = std::get<1>(*size_it); + + // The canonical form search below runs once per beta and costs a lot for 8-bit tables (about 20 seconds + // for one that matches nothing), so first rule out the tables that no entry can match at all: every entry + // of the same equivalence class has the same differential spectrum. + const auto spectrum = differential_spectrum(sbox); + if (std::none_of(matching_size_data.cbegin(), matching_size_data.cend(), [&spectrum](const auto& entry) { return differential_spectrum(entry.first) == spectrum; })) + { + return ERR("no match found within database"); + } // beta has to count beyond the largest table index, so it must be wider than a table entry: a u8 stays // below a size of 256 forever, which made this loop endless for every 8-bit S-box not in the database @@ -498,14 +542,20 @@ namespace hal auto lin_rep = compute_linear_representative_bounded(sbox_beta, budget); if (budget == 0) { - // XORing a constant onto the outputs does not change how close to linear the table is, so if the - // search degenerates for one beta it degenerates for all of them, and no real S-box ever does + if (sbox_beta.front() == 0) + { + // A table that maps 0 to 0 takes a search of its own (see compute_linear_representative_bounded), + // which can exhaust the budget for this one beta only, e.g., for AES XORed with 0x63, which is + // linearly equivalent to inversion in GF(2^8). The other betas still find such an S-box. + continue; + } + // apart from that, XORing a constant onto the outputs does not change how close to linear the + // table is, so if the search degenerates for one beta it degenerates for all of them log_info("hawkeye", "giving up the S-box lookup, as the table is too close to linear to be a real S-box."); break; } - const auto& matching_size_data = std::get<1>(*size_it); - const auto rep_it = matching_size_data.find(lin_rep); + const auto rep_it = matching_size_data.find(lin_rep); if (rep_it != matching_size_data.end()) { return OK(rep_it->second.front().first);