Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions plugins/dataflow_analysis/src/pre_processing/pre_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,48 @@ namespace hal
// find successors
std::unordered_map<const Net*, std::pair<std::unordered_set<Gate*>, std::unordered_set<u32>>> suc_cache;
std::unordered_map<const Net*, std::unordered_set<u32>> 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<const Net*>& 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<const Net*>& 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<const Net*>& 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++;
Expand Down Expand Up @@ -203,6 +245,10 @@ namespace hal
stack.push_back(n);
added = true;
}
else
{
pass_on_successors(n, current, previous);
}
}
}
}
Expand Down Expand Up @@ -279,6 +325,10 @@ namespace hal
stack.push_back(n);
added = true;
}
else
{
pass_on_predecessors(n, current, previous);
}
}
}
}
Expand Down
72 changes: 44 additions & 28 deletions plugins/hawkeye/src/cipher_candidate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<std::string> lookup_sbox(const std::vector<std::vector<BooleanFunction::Value>>& rows, const u32 num_inputs, const SBoxDatabase& db)
Expand Down Expand Up @@ -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<std::bitset<MAX_SBOX_ROWS>> 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<std::bitset<MAX_SBOX_ROWS>> basis;
std::vector<u32> pivots;
const auto add_if_independent = [&basis, &pivots, num_rows](std::bitset<MAX_SBOX_ROWS> 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<MAX_SBOX_ROWS> constant;
for (u32 row = 0; row < num_rows; row++)
{
constant[row] = 1;
}
add_if_independent(constant);

std::vector<u32> independent;
for (u32 out = 0; out < num_outputs; out++)
{
if (mat[out].any())
std::bitset<MAX_SBOX_ROWS> vec;
for (u32 row = 0; row < num_rows; row++)
{
vec[row] = (values[row] >> out) & 1;
}
if (add_if_independent(vec))
{
independent.push_back(out);
}
Expand Down Expand Up @@ -415,7 +431,7 @@ namespace hal
const std::vector<Gate*> 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;
Expand Down Expand Up @@ -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));
}
Expand Down
58 changes: 54 additions & 4 deletions plugins/hawkeye/src/sbox_database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"

#include <algorithm>
#include <cmath>
#include <fstream>
#include <iostream>
Expand Down Expand Up @@ -296,6 +297,32 @@ namespace hal
constexpr u64 LINEAR_REPRESENTATIVE_BUDGET = 250000;

std::vector<u8> compute_linear_representative_bounded(const std::vector<u8>& 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<u32> differential_spectrum(const std::vector<u8>& sbox)
{
const u32 len = sbox.size();
std::vector<u32> spectrum(len + 1, 0);
std::vector<u32> 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<std::string, std::vector<u8>>& sboxes)
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
Loading