Skip to content

HAWKEYE

Julian Speith edited this page Aug 18, 2026 · 19 revisions

HAWKEYE is a tool to automatically locate implementations of symmetric cryptographic primitives within gate-level netlists. It was developed as part of a publication titled "HAWKEYE - Recovering Symmetric Cryptography From Hardware Circuits" that will be presented at CRYPTO'24. Currently, HAWKEYE is designed to find round-based and pipelined implementations of SPN, ARX and Feistel ciphers. It is not particularly well suited for shift-register-based ciphers and implementations protected against side-channel attacks, although it might get lucky at times.

Prerequisites and preprocessing

For HAWKEYE to be effective, all gate types used by the netlist under analysis must be properly annotated within the gate library. In particular, each gate must feature

  • Boolean functions describing its outputs (combinational gates only)
  • one or more annotated GateTypeProperty tags for each gate type
  • a correctly assigned PinType for each pin, particularly those of flip-flops

As HAWKEYE cannot currently handle combinational gates with multiple outputs, such gates must be split into two or more separate logic gates before analysis takes place. For example, Xilinx 7-series FPGAs feature LUT6_2 gates that can implement a LUT5 alongside a LUT6. Before running HAWKEYE, we need to split these LUTs up using the split_luts function from the xilinx_toolbox plugin. Furthermore, we remove fan-in endpoints from LUTs if they do not contribute to their implemented Boolean function to aid structural analysis using remove_unused_lut_inputs from the netlist_preprocessing plugin.

from hal_plugins import xilinx_toolbox
from hal_plugins import netlist_preprocessing

xilinx_toolbox.split_luts(netlist)
netlist_preprocessing.remove_unused_lut_inputs(netlist)

Independent of whether an ASIC or FPGA netlist is analyzed, we recommend removing buffer gates by calling remove_buffers to again support structural analysis. Furthermore, some gate libraries feature flip-flops that come with two outputs, one carrying the inverted signal of the other. Here, we recommend using unify_ff_outputs, which will reconnect the inverted flip-flop output to the non-inverted one after inserting an additional inverter gate. This way, the inversion of the output is moved into combinational logic, which allows HAWKEYE to properly deal with it.

netlist_preprocessing.remove_buffers(netlist)
netlist_preprocessing.unify_ff_outputs(netlist)

Additional preprocessing steps may be necessary depending on the netlist-under-analysis. Sometimes, it may also make sense to break LUTs up into primitives combinational gates by decomposing or resynthesizing the respective LUTs. This could aid the localization of S-boxes in particular.

Detecting candidates for state registers

To identify candidates for state registers of pipelined or round-based implementations of symmetric ciphers, HAWKEYE converts the input netlist into a flip-flop graph, that is, a graph containing a vertex for every flip-flop in the netlist and an edge between two vertices only if the respective flip-flops are connected through combinational logic. This graph can be further refined by only adding edges if two connected flip-flops are of the same type or are controlled by the same input pins or nets.

HAWKEYE then computes the k-th neighborhood of every flip-flop for k=1, ..., timeout (with timeout defaulting to 10) and outputs a candidate if two successive neighborhoods of the same flip-flop are of equal size (and are larger than min_register_size), that is the i-th neighborhood has the same size as the (i+1)-th neighborhood. Together with the check whether two connected flip-flops feature the same control nets, this makes up Method 1 from the paper.

from hal_plugins import hawkeye

c_nets = hawkeye.DetectionConfiguration()
c_nets.control = hawkeye.DetectionConfiguration.Control.CHECK_NETS
c_nets.components = hawkeye.DetectionConfiguration.Components.NONE
c_nets.timeout = 10
c_nets.min_register_size = 10

candidates = hawkeye.CipherCandidate.detect(netlist, [c_nets], min_state_size=40)

Sometimes it helps to relax the control configuration to Control.CHECK_PINS instead of Control.CHECK_NETS, which will just check whether the same pins of flip-flops are used, but they no longer have to be connected to the same control nets. This can also be done in combination with Control.CHECK_NETS.

from hal_plugins import hawkeye

c_nets = hawkeye.DetectionConfiguration()
c_nets.control = hawkeye.DetectionConfiguration.Control.CHECK_NETS
...

c_pins = hawkeye.DetectionConfiguration()
c_pins.control = hawkeye.DetectionConfiguration.Control.CHECK_PINS
...

candidates = hawkeye.CipherCandidate.detect(netlist, [c_nets, c_pins], min_state_size=40)

If flip-flops are controlled synchronously, their control inputs are often moved into combinational logic, which makes it harder to differentiate between flip-flops that do not belong to the same register. In the paper, we observed this being the case for some of our ASIC benchmarks. To address this issue, HAWKEYE supports the detection of strongly connected components (SCCs) within the neighborhoods while they are computed to refine results. As the flip-flops of a round-based implementation usually form an SCC, this approach ensures that only the state flip-flops end up in the state register candidate. In the paper, this refinement is referred to as Method 2.

Additionally, some gate libraries feature functionally equivalent gate types that only differ in their electrical properties. As this hampers functional analysis, HAWKEYE allows to specify such gates such that type checks on these gates allow for them to be used interchangeably.

from hal_plugins import hawkeye

config = hawkeye.DetectionConfiguration()
config.control = hawkeye.DetectionConfiguration.Control.CHECK_TYPE
config.components = hawkeye.DetectionConfiguration.Components.CHECK_SCC
config.equivalent_types = [["FD1", "FD1P"]]
config.timeout = 10
config.min_register_size = 10

candidates = hawkeye.CipherCandidate.detect(netlist, [config], min_state_size=40)

A candidate discovered this way only knows its state register, which can be retrieved with get_input_reg() and get_output_reg(), e.g., to create respective modules in the netlist. Both return the flip-flops ordered by gate ID, and for a round-based implementation the two are identical. Everything beyond the register is filled in by the analysis steps below, so use has_round_function() and get_sboxes() to find out how far a candidate has been analyzed.

Isolate round function

To isolate the round function for further analysis, call build_round_function() on a candidate. This determines the combinational logic computing the next state, i.e., the gates between the input and the output register, which can then be retrieved with get_round_logic().

for c in candidates:
    if not c.build_round_function():
        continue

The analysis operates on the netlist under analysis itself, so all gates and nets of a candidate are the ones of that netlist and can be inspected or grouped into modules directly, without any mapping. For a round-based implementation, where the input register is also the output register, the feedback would close a cycle through every state flip-flop. Rather than copying the register, HAWKEYE represents each of its flip-flops by two vertices in the graph it analyzes: a primary vertex carrying the outgoing edges and a shadow vertex carrying the incoming ones. The graph is available through get_graph().

While computing the round function, HAWKEYE also annotates its state input and output nets, any control nets, and other nets (e.g., plaintext and (round)key) feeding into the combinational round function logic. These can be retrieved using get_state_inputs(), get_state_outputs(), get_control_inputs(), and get_other_inputs().

Locate and identify S-Boxes

Once the combinational logic of the round function has been isolated and its inputs have been annotated, HAWKEYE can be tasked to search for known S-boxes within the round function of a candidate. To this end, it first needs to load or create a database of known S-boxes. This database comprises linear representatives of many S-boxes of up to 8 bits, against which HAWKEYE will later match S-box candidates extracted from logic (under affine equivalence).

A ready-to-use database ships with HAL. Its source lives at plugins/hawkeye/sbox-database.json and the build copies it to <build>/share/hal/sbox-database.json, which is where you should load it from at runtime. It currently contains seven S-boxes, grouped by bit size:

Bit size S-boxes
4 GIFT, PRESENT, PRINCE, Piccolo, SKINNY_4
5 Ascon
8 AES

Point from_file() at that path, or at your own database if you have built one (see below).

After the database has been set up, HAWKEYE can search for S-boxes using structural analysis using locate_sboxes(). This produces the S-boxes it finds, possibly none, each of which comprises of input flip-flops and some combinational logic and come with annotated input and output gates, available as input_gates, output_gates and component. Since the exact size and shape of an S-box is not known in advance, the search deliberately produces more S-boxes than the round function actually contains, among them several variants of the same S-box that differ only in which of the surplus gates are taken as its outputs.

identify_sboxes() then matches them against the database using functional analysis and annotates each one with the outcome, which is either identified (see identified_as), unidentified, or superseded if another variant of the same S-box was identified before it. It returns the number of S-boxes identified.

import os

sbox_db = hawkeye.SBoxDatabase.from_file(os.path.join(str(hal_py.CoreUtils.get_share_directory()), "sbox-database.json"))

for c in candidates:
    c.locate_sboxes()
    c.identify_sboxes(sbox_db)

    for sbox in c.get_sboxes():
        if sbox.status == hawkeye.SBoxStatus.identified:
            print(sbox.identified_as, [g.id for g in sbox.input_gates])

To identify a single S-box without annotating it, use identify_sbox() on the candidate instead. Note that an S-box that simply does not match anything in the database is not an error: in that case an empty string is returned.

Writing the results back into the netlist

Finally, create_modules() writes a candidate back into the netlist as a module hierarchy: one module holding the entire candidate, a submodule holding its state register, and one submodule per identified S-box holding its combinational gates.

for c in candidates:
    module = c.create_modules()

Creating and editing an S-Box database

You can also create your own S-box database, e.g., by first creating an empty database and then filling it up with known S-boxes (up to 8 bits). For PRESENT, this could look like shown in the Python code below. Note that you can also save your database to a JSON file and load it just as shown before.

sbox_db = hawkeye.SBoxDatabase()

present_sbox = [0xC,0x5,0x6,0xB,0x9,0x0,0xA,0xD,0x3,0xE,0xF,0x8,0x4,0x7,0x1,0x2]

sbox_db.add("PRESENT", present_sbox)
sbox_db.store("my-sbox-database.json")

See also

Clone this wiki locally