Module logiclocking.locks
Apply logic locks to circuits.
Expand source code
"""Apply logic locks to circuits."""
import random
from itertools import product, zip_longest
from random import choice, choices, randint, sample, shuffle
import circuitgraph as cg
from pysat.card import CardEnc
from pysat.formula import IDPool
from pysat.solvers import Cadical
def trll(c, keylen, s1_s2_ratio=1, shuffle_key=True, seed=None):
"""
Locks a circuitgraph with Truly Random Logic Locking.
Limaye, E. Kalligeros, N. Karousos, I. G. Karybali and O. Sinanoglu,
"Thwarting All Logic Locking Attacks: Dishonest Oracle With Truly Random
Logic Locking," in IEEE Transactions on Computer-Aided Design of Integrated
Circuits and Systems, vol. 40, no. 9, pp. 1740-1753, Sept. 2021.
Parameters
----------
c: circuitgraph.Circuit
The circuit to lock.
keylen: int
The number of key bits to add.
s1_s2_ratio: int or str
The ratio between number of key gate locations locked where an
inverter exists in the original design (s1) or where an inverter
does not exist in the original design (s2). The paper leaves this
value at 1 (meaning s1=s2=keylen/2), but they note that this
could be adjusted based on the ratio of the locations where there
is an inverter in the original netlist. Setting this parameter
to the string "infer" will do this adjustment. I.e.
s1 = keylen*r, s2 = keylen*(1-r), where r is the number of
inverters in the circuit divided by the total number of gates.
shuffle_key: bool
By default, the key input labels are shuffled at the end of the
algorithm so the labelling does not reveal which portion of the
algorithm the key input was added during.
seed: int
Seed for the random selection of gates and shuffling of the key.
Returns
-------
circuitgraph.Circuit, dict of str:bool
The locked circuit and the correct key value for each key input.
"""
rng = random.Random(seed)
cl = c.copy()
if keylen % 2 != 0:
raise NotImplementedError
if s1_s2_ratio == "infer":
raise NotImplementedError
s1 = int((keylen // 2) * s1_s2_ratio)
if s1 > keylen:
raise ValueError(f"Unusable s1_s2_ratio: {s1_s2_ratio}")
s2 = keylen - s1
s1a = rng.randint(0, s1)
s1b = s1 - s1a
s2a = rng.randint(0, s2)
s2b = s2 - s2a
inv_gates = list(c.filter_type("not"))
rng.shuffle(inv_gates)
rem_gates = list(
c.nodes()
- c.io()
- c.filter_type(("not", "bb_input", "bb_output", "0", "1", "x"))
)
rng.shuffle(rem_gates)
j = 0
k = {}
# Replace existing inv_gates with XOR key-gates
for _ in range(s1a):
sel_gate = inv_gates.pop()
ki = f"key_{j}"
cl.add(ki, "input")
k[ki] = True
cl.set_type(sel_gate, "xor")
cl.connect(ki, sel_gate)
j += 1
# Add XOR key-gates before existing inv_gates
for _ in range(s1b):
sel_gate = inv_gates.pop()
ki = f"key_{j}"
cl.add(ki, "input")
k[ki] = False
inv_fanin = cl.fanin(sel_gate)
assert len(inv_fanin) == 1
cl.disconnect(inv_fanin, sel_gate)
cl.add(f"key_gate_{j}", "xor", fanin=inv_fanin | {ki}, fanout=sel_gate)
j += 1
# Add XOR key-gates and INV gates after existing rem_gates
for _ in range(s2a):
sel_gate = rem_gates.pop()
ki = f"key_{j}"
cl.add(ki, "input")
k[ki] = True
sel_fanout = cl.fanout(sel_gate)
cl.disconnect(sel_gate, sel_fanout)
cl.add(f"key_gate_{j}", "xor", fanin=(sel_gate, ki))
cl.add(f"key_inv_{j}", "not", fanin=f"key_gate_{j}", fanout=sel_fanout)
j += 1
# Add XOR key-gates after existing rem_gates
for _ in range(s2b):
sel_gate = rem_gates.pop()
ki = f"key_{j}"
cl.add(ki, "input")
k[ki] = False
sel_fanout = cl.fanout(sel_gate)
cl.disconnect(sel_gate, sel_fanout)
cl.add(f"key_gate_{j}", "xor", fanin=(sel_gate, ki), fanout=sel_fanout)
j += 1
# Shuffle keys
if shuffle_key:
new_order = list(range(keylen))
rng.shuffle(new_order)
shuffled_k = {}
intermediate_mapping = {}
final_mapping = {}
for old_idx, new_idx in enumerate(new_order):
shuffled_k[f"key_{new_idx}"] = k[f"key_{old_idx}"]
intermediate_mapping[f"key_{old_idx}"] = f"key_{old_idx}_temp"
final_mapping[f"key_{old_idx}_temp"] = f"key_{new_idx}"
cl.relabel(intermediate_mapping)
cl.relabel(final_mapping)
return cl, shuffled_k
return cl, k
def xor_lock(c, keylen, key_prefix="key_", replacement=False):
"""
Locks a circuitgraph with a random xor lock.
J. A. Roy, F. Koushanfar and I. L. Markov, "Ending Piracy of Integrated
Circuits," in Computer, vol. 43, no. 10, pp. 30-38, Oct. 2010.
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
keylen: int
the number of bits in the key
replacement: bool
If True, the same line can be locked twice (resulting in a chain
of key gates)
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
# randomly select gates to lock
if replacement:
gates = choices(tuple(cl.nodes() - cl.outputs()), k=keylen)
else:
gates = sample(tuple(cl.nodes() - cl.outputs()), keylen)
# insert key gates
key = {}
for i, gate in enumerate(gates):
# select random key value
key[f"{key_prefix}{i}"] = choice([True, False])
# create xor/xnor,input
gate_type = "xnor" if key[f"{key_prefix}{i}"] else "xor"
fanout = cl.fanout(gate)
cl.disconnect(gate, fanout)
cl.add(f"key_gate_{i}", gate_type, fanin=gate, fanout=fanout)
cl.add(f"{key_prefix}{i}", "input", fanout=f"key_gate_{i}")
cg.lint(cl)
return cl, key
def mux_lock(c, keylen, avoid_loops=False, key_prefix="key_"):
"""
Locks a circuitgraph with a mux lock.
J. Rajendran et al., "Fault Analysis-Based Logic Encryption," in IEEE
Transactions on Computers, vol. 64, no. 2, pp. 410-424, Feb. 2015,
doi: 10.1109/TC.2013.193.
Note that a random mux selection is used, not fault-based.
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
keylen: int
the number of bits in the key.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
# get 2:1 mux
m = cg.logic.mux(2)
# randomly select gates
gates = sample(tuple(cl.nodes() - cl.outputs()), keylen)
if avoid_loops:
decoy_gates = set()
else:
decoy_gates = sample(tuple(cl.nodes() - cl.outputs()), keylen)
# insert key gates
key = {}
for i, gate in enumerate(gates):
# select random key value
key_val = choice([True, False])
if avoid_loops:
decoy_gate = choice(
tuple(
c.nodes()
- c.io()
- set(gates)
- cl.transitive_fanout(gate)
- decoy_gates
)
)
decoy_gates.add(decoy_gate)
else:
decoy_gate = decoy_gates[i]
# create and connect mux
fanout = cl.fanout(gate)
cl.disconnect(gate, fanout)
cl.add_subcircuit(m, f"mux_{i}")
cl.connect(f"mux_{i}_out", fanout)
key_in = cl.add(f"{key_prefix}{i}", "input", fanout=f"mux_{i}_sel_0", uid=True)
key[key_in] = key_val
if key_val:
cl.connect(gate, f"mux_{i}_in_1")
cl.connect(decoy_gate, f"mux_{i}_in_0")
else:
cl.connect(gate, f"mux_{i}_in_0")
cl.connect(decoy_gate, f"mux_{i}_in_1")
cg.lint(cl)
if avoid_loops and cl.is_cyclic():
raise ValueError("Locked circuit is cyclic")
return cl, key
def random_lut_lock(c, num_gates, lut_width, gates=None):
"""
Locks a circuitgraph by replacing random gates with LUTs.
This is kind of like applying LUT-lock with no replacement strategy.
(H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan,
"LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and
ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on
VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410.)
Parameters
----------
circuit: circuitgraph.CircuitGraph
Circuit to lock.
num_gates: int
the number of gates to lock.
lut_width: int
LUT width, defines maximum fanin of locked gates.
gates: list of str
The gates to lock. If not provided, will be randomly sampled
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
# parse mux
m = cg.logic.mux(2 ** lut_width)
# randomly select gates
potential_gates = {g for g in cl.nodes() - cl.io() if len(cl.fanin(g)) <= lut_width}
if gates:
if len(gates) != num_gates:
raise ValueError(
f"Got 'num_gates' of {num_gates} but length of "
f"'gates' is {len(gates)}"
)
if any(len(cl.fanin(g)) > lut_width for g in gates):
raise ValueError("cannot lock a gate with fanin greater than " "lut_width")
if any(g in cl.io() for g in gates):
raise ValueError("cannot lock an input/output gate")
else:
gates = sample(tuple(potential_gates), num_gates)
potential_gates -= set(gates)
potential_gates -= cl.transitive_fanout(gates)
# insert key gates
key = {}
for i, gate in enumerate(gates):
fanout = list(cl.fanout(gate))
fanin = list(cl.fanin(gate))
try:
padding = sample(
tuple(potential_gates - cl.fanin(gate)), lut_width - len(fanin)
)
except ValueError as e:
raise ValueError("Could not find enough viable gates for padding") from e
# create LUT
cl.add_subcircuit(m, f"lut_{i}")
# connect keys
for j, vs in enumerate(product([False, True], repeat=len(fanin + padding))):
assumptions = {
s: v for s, v in zip(fanin + padding, vs[::-1]) if s in fanin
}
key_in = cl.add(
f"key_{i*2**lut_width+j}", "input", fanout=f"lut_{i}_in_{j}", uid=True
)
result = cg.sat.solve(c, assumptions)
if not result:
key[key_in] = False
else:
key[key_in] = result[gate]
# connect out
cl.disconnect(gate, fanout)
cl.connect(f"lut_{i}_out", fanout)
# connect sel
for j, f in enumerate(fanin + padding):
cl.connect(f, f"lut_{i}_sel_{j}")
# delete gate
cl.remove(gate)
cl = cg.tx.relabel(cl, {f"lut_{i}_out": gate})
cg.lint(cl)
return cl, key
def lut_lock(
c,
num_gates,
count_keys=False,
skip_fi1=True,
rank_by_shared_fanin=False,
key_prefix="key_",
):
"""
Locks a circuitgraph with NB2-MO-HSC LUT-lock.
H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan,
"LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and
ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on
VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410.
Parameters
----------
circuit: circuitgraph.CircuitGraph
Circuit to lock.
num_gates: int
The number of gates to lock.
count_keys: bool
If true, continue locking until at least `num_gates` keys are
added instead of `num_gates` gates.
skip_fi1: int
If True, nodes with a fanin of 1 (i.e. buf or inv) will not
be considered for locking.
rank_by_shared_fanin: bool
If True, the output with the least shared fanin with other outputs
will be selected for locking first. By default, the output with
the least amount of total fanin is selected for locking first.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
Raises
------
ValueError
if there are not enough viable gates to lock.
"""
# create copy to lock
cl = c.copy()
def calc_skew(gate, cl):
d = {False: 0, True: 0}
fanin = list(cl.fanin(gate))
# create subcircuit containing just gate for simulation
simc = cg.Circuit()
for i in fanin:
simc.add(i, "input")
simc.add(gate, cl.type(gate), fanin=fanin)
# simulate
for i, vs in enumerate(product([False, True], repeat=len(fanin))):
assumptions = dict(zip(fanin, vs[::-1]))
result = cg.sat.solve(simc, assumptions)
if not result:
d[False] += 1
else:
d[result[gate]] += 1
num_combos = 2 ** len(fanin)
return abs(d[False] / num_combos - d[True] / num_combos)
def replace_lut(gate, cl):
key = {}
m = cg.logic.mux(2 ** len(cl.fanin(gate)))
fanout = list(cl.fanout(gate))
fanin = list(cl.fanin(gate))
# create LUT
cl.add_subcircuit(m, f"lut_{gate}")
# create subcircuit containing just gate for simulation
simc = cg.Circuit()
for i in fanin:
simc.add(i, "input")
simc.add(gate, cl.type(gate), fanin=fanin)
# connect keys
for i, vs in enumerate(product([False, True], repeat=len(fanin))):
assumptions = dict(zip(fanin, vs[::-1]))
cl.add(f"{key_prefix}{gate}_{i}", "input", fanout=f"lut_{gate}_in_{i}")
result = cg.sat.solve(simc, assumptions)
if not result:
key[f"{key_prefix}{gate}_{i}"] = False
else:
key[f"{key_prefix}{gate}_{i}"] = result[gate]
# connect out
cl.disconnect(gate, fanout)
cl.connect(f"lut_{gate}_out", fanout)
# connect sel
for i, f in enumerate(fanin):
cl.connect(f, f"lut_{gate}_sel_{i}")
# delete gate
cl.remove(gate)
return key, [f"lut_{gate}_{n}" for n in m.nodes()], f"lut_{gate}_out"
def continue_locking(locked_gates, num_gates, keys, count_keys):
if count_keys:
return len(keys) < num_gates
return locked_gates < num_gates
locked_gates = 0
outputs = list(cl.outputs())
if rank_by_shared_fanin:
def rank_output(x):
other_outputs = [o for o in outputs if o != x]
other_fanin = cl.transitive_fanin(other_outputs)
curr_fanin = cl.transitive_fanin(x)
return len(curr_fanin & other_fanin)
else:
def rank_output(x):
return len(cl.transitive_fanin(x))
outputs.sort(key=rank_output)
candidates = []
forbidden_nodes = set()
keys = {}
while continue_locking(locked_gates, num_gates, keys, count_keys):
if not candidates:
outputs = [o for o in outputs if o not in forbidden_nodes]
try:
candidates.append(outputs.pop(0))
except IndexError as e:
raise ValueError(
"Ran out of candidate gates at " f"{locked_gates} gates."
) from e
else:
candidate = candidates.pop(0)
candidate_is_output = cl.is_output(candidate)
children = cl.fanin(candidate)
if candidate in forbidden_nodes:
candidates += [g for g in children if g not in forbidden_nodes]
continue
forbidden_nodes.add(candidate)
if len(children) == 0:
continue
if skip_fi1 and len(children) == 1:
child = children.pop()
if child not in forbidden_nodes | set(candidates):
candidates.insert(0, child)
continue
key, nodes, output_to_relabel = replace_lut(candidate, cl)
keys.update(key)
forbidden_nodes.update(nodes)
cl = cg.tx.relabel(cl, {output_to_relabel: candidate})
if candidate_is_output:
cl.set_output(candidate)
for g1 in children:
forbidden_nodes.add(g1)
for g2 in cl.fanin(g1):
if g2 not in forbidden_nodes | set(candidates):
candidates.append(g2)
# Sort by least number of outputs in fanout cone, then most skew
candidates.sort(
key=lambda x: (
len(cl.transitive_fanout(x) & cl.outputs()),
-calc_skew(x, cl),
)
)
locked_gates += 1
return cl, keys
def tt_lock(c, width, target_output=None):
"""
Locks a circuitgraph with TTLock.
Note that in this implementation the original circuit is not
functionally stripped, meaning that it does not produce an inverted
response for the protected input pattern. This makes this implementation
vulnurable to removal attacks. However, it can still be used to measure
SAT attack resiliance.
M. Yasin, A. Sengupta, B. Schafer, Y. Makris, O. Sinanoglu, and
J. Rajendran, “What to Lock?: Functional and Parametric Locking,”
in Great Lakes Symposium on VLSI, pp. 351–356, 2017.
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
width: int
the minimum fanin of the gates to lock.
target_output: str
If defined, this output will be the one which is locked.
Otherwise, a random output will be locked.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
if len(c.inputs()) < width:
raise ValueError(f"Not enough inputs to lock with width '{width}'")
if not target_output:
target_output = random.choice(list(cl.outputs()))
# get inputs to lock
target_inputs = cl.startpoints(target_output)
if len(target_inputs) < width:
target_inputs |= set(
random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs))
)
target_inputs = list(target_inputs)
# create key
key = {f"key_{i}": random.choice([True, False]) for i in range(width)}
# connect comparators
cl.add("flip_out", "and")
cl.add("restore_out", "and")
for i, inp in enumerate(random.sample(target_inputs, width)):
cl.add(f"key_{i}", "input")
cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0")
cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp], fanout="restore_out")
cl.add(
f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp], fanout="flip_out"
)
# flip output
old_out = cl.uid(f"{target_output}_pre_lock")
cl = cg.tx.relabel(cl, {target_output: old_out})
cl.set_output(old_out, False)
cl.add(
target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True
)
cg.lint(cl)
return cl, key
def tt_lock_sen(c, width, nsamples=10):
"""
Locks a circuitgraph with TTLock-Sen.
Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi,
“Sensitivity Analysis of Locked Circuits,” in
Logic for Programming, Artificial Intelligence and Reasoning
(LPAR-23), pp. 483-497. EPiC Series in Computing 73, EasyChair.
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
width: int
the minimum fanin of the gates to lock.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
# find output with large enough fanin
potential_outs = [o for o in cl.outputs() if len(cl.startpoints(o)) >= width]
if not potential_outs:
raise ValueError(f"Not enough inputs to lock with width '{width}'")
# find average sensitivities
A = {}
N = {}
S = {}
for o in potential_outs:
# build sensitivity circuit
s = cg.tx.sensitivity_transform(c, o)
startpoints = c.startpoints(o)
s_out = {o for o in s.outputs() if "difference" in o}
# est avg sensitivity
total = 0
for i in range(nsamples):
input_val = {i: randint(0, 1) for i in startpoints}
model = cg.sat.solve(s, input_val)
sen = sum(model[o] for o in s_out)
total += sen
A[o] = int(total / nsamples)
N[o] = len(startpoints)
S[o] = s
# find output + input value with closest to avg sen
def find_input():
b = 0
while b < max(N.values()):
for o in potential_outs:
upper = min(N[o], int(N[o] - A[o] + b))
lower = max(0, int(N[o] - A[o] - b))
us = cg.utils.int_to_bin(upper, cg.utils.clog2(N[o]))
ls = cg.utils.int_to_bin(lower, cg.utils.clog2(N[o]))
for sv in [us, ls]:
model = cg.sat.solve(
S[o], {f"sen_out_{i}": v for i, v in enumerate(sv)}
)
if model:
out = o
startpoints = c.startpoints(o)
key = {f"key_{i}": model[n] for i, n in enumerate(startpoints)}
return key, startpoints, out
b += 1
key, startpoints, out = find_input()
# connect comparators
cl.add("flip_out", "and")
cl.add("restore_out", "and")
for i, inp in enumerate(startpoints):
cl.add(f"key_{i}", "input")
cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0")
cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp], fanout="restore_out")
cl.add(
f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp], fanout="flip_out"
)
# flip output
old_out = cl.uid(f"{out}_pre_lock")
cl = cg.tx.relabel(cl, {out: old_out})
cl.set_output(old_out, False)
cl.add(out, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True)
cg.lint(cl)
return cl, key
def sfll_hd(c, width, hd, target_output=None):
"""
Locks a circuitgraph with SFLL-HD.
Note that in this implementation the original circuit is not
functionally stripped, meaning that it does not produce an inverted
response for the protected input pattern. This makes this implementation
vulnurable to removal attacks. However, it can still be used to measure
SAT attack resiliance.
Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel, Mohammed Ashraf,
Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017. Provably-Secure
Logic Locking: From Theory To Practice. In Proceedings of the 2017 ACM
SIGSAC Conference on Computer and Communications Security (CCS ’17).
Association for Computing Machinery, New York, NY, USA, 1601–1618.
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
width: int
key width, also the minimum fanin of the gates to lock.
hd: int
the hamming distance to lock with, as explained in the paper.
target_output: str
If defined, this output will be the one which is locked.
Otherwise, a random output will be locked.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
# parse popcount
p = cg.logic.popcount(width)
if len(c.inputs()) < width:
raise ValueError(f"Not enough inputs to lock with width '{width}'")
if not target_output:
target_output = random.choice(list(cl.outputs()))
# get inputs to lock
target_inputs = cl.startpoints(target_output)
if len(target_inputs) < width:
target_inputs |= set(
random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs))
)
target_inputs = list(target_inputs)
# create key
key = {f"key_{i}": random.choice([True, False]) for i in range(width)}
# instantiate and connect hd circuits
cl.add_subcircuit(p, "flip_pop")
cl.add_subcircuit(p, "restore_pop")
# connect inputs
for i, inp in enumerate(random.sample(target_inputs, width)):
cl.add(f"key_{i}", "input")
cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0")
cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp])
cl.add(f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp])
cl.connect(f"flip_xor_{i}", f"flip_pop_in_{i}")
cl.connect(f"restore_xor_{i}", f"restore_pop_in_{i}")
# connect outputs
cl.add("flip_out", "and")
cl.add("restore_out", "and")
for i, v in enumerate(format(hd, f"0{cg.utils.clog2(width)+1}b")[::-1]):
cl.add(f"hd_{i}", v)
cl.add(
f"restore_out_xnor_{i}",
"xnor",
fanin=[f"hd_{i}", f"restore_pop_out_{i}"],
fanout="restore_out",
)
cl.add(
f"flip_out_xnor_{i}",
"xnor",
fanin=[f"hd_{i}", f"flip_pop_out_{i}"],
fanout="flip_out",
)
# flip output
old_out = cl.uid(f"{target_output}_pre_lock")
cl = cg.tx.relabel(cl, {target_output: old_out})
cl.set_output(old_out, False)
cl.add(
target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True
)
cg.lint(cl)
return cl, key
def sfll_flex(c, width, n, target_output=None):
"""
Locks a circuitgraph with SFLL-flex.
Note that in this implementation the original circuit is not
functionally stripped, meaning that it does not produce an inverted
response for the protected input pattern. This makes this implementation
vulnurable to removal attacks. However, it can still be used to measure
SAT attack resiliance.
Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel,
Mohammed Ashraf, Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017.
Provably-Secure Logic Locking: From Theory To Practice. In Proceedings of
the 2017 ACM SIGSAC Conference on Computer and Communications Security.
Association for Computing Machinery, New York, NY, USA, 1601–1618.
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
width: int
the minimum fanin of the gates to lock.
n: int
number of input patterns to lock.
target_output: str
If defined, this output will be the one which is locked.
Otherwise, a random output will be locked.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
if not target_output:
target_output = random.choice(list(cl.outputs()))
# get inputs to lock
target_inputs = cl.startpoints(target_output)
if len(target_inputs) < width:
target_inputs |= set(
random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs))
)
target_inputs = list(target_inputs)
# create key
key = {f"key_{i}": choice([True, False]) for i in range(width * n)}
# connect comparators
cl.add("flip_out", "or")
cl.add("restore_out", "or")
for j in range(n):
cl.add(f"flip_and_{j}", "and", fanout="flip_out")
cl.add(f"restore_and_{j}", "and", fanout="restore_out")
for i, inp in enumerate(random.sample(target_inputs, width)):
for j in range(n):
cl.add(f"key_{i+j*width}", "input")
cl.add(f"hardcoded_key_{i}_{j}", "1" if key[f"key_{i+j*width}"] else "0")
cl.add(
f"restore_xor_{i}_{j}",
"xor",
fanin=[f"key_{i+j*width}", inp],
fanout=f"restore_and_{j}",
)
cl.add(
f"flip_xor_{i}_{j}",
"xor",
fanin=[f"hardcoded_key_{i}_{j}", inp],
fanout=f"flip_and_{j}",
)
# flip output
old_out = cl.uid(f"{target_output}_pre_lock")
cl = cg.tx.relabel(cl, {target_output: old_out})
cl.set_output(old_out, False)
cl.add(
target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True
)
cg.lint(cl)
return cl, key
def _connect_banyan(cl, swb_ins, swb_outs, bw):
I = int(2 * cg.utils.clog2(bw) - 2)
J = int(bw / 2)
for i in range(cg.utils.clog2(J)):
r = J / (2 ** i)
for j in range(J):
t = (j % r) >= (r / 2)
# straight
out_i = int((i * bw) + (2 * j) + t)
in_i = int((i * bw + bw) + (2 * j) + t)
cl.connect(swb_outs[out_i], swb_ins[in_i])
# cross
out_i = int((i * bw) + (2 * j) + (1 - t) + ((r - 1) * ((1 - t) * 2 - 1)))
in_i = int((i * bw + bw) + (2 * j) + (1 - t))
cl.connect(swb_outs[out_i], swb_ins[in_i])
if r > 2:
# straight
out_i = int(((I * J * 2) - ((2 + i) * bw)) + (2 * j) + t)
in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + t)
cl.connect(swb_outs[out_i], swb_ins[in_i])
# cross
out_i = int(
((I * J * 2) - ((2 + i) * bw))
+ (2 * j)
+ (1 - t)
+ ((r - 1) * ((1 - t) * 2 - 1))
)
in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + (1 - t))
cl.connect(swb_outs[out_i], swb_ins[in_i])
def _connect_banyan_bb(cl, swb_ins, swb_outs, bw):
I = int(2 * cg.utils.clog2(bw) - 2)
J = int(bw / 2)
for i in range(cg.utils.clog2(J)):
r = J / (2 ** i)
for j in range(J):
t = (j % r) >= (r / 2)
# straight
out_i = int((i * bw) + (2 * j) + t)
in_i = int((i * bw + bw) + (2 * j) + t)
cl.add(
f"swb_{i}_{j}_straight",
"buf",
fanin=swb_outs[out_i],
fanout=swb_ins[in_i],
)
# cross
out_i = int((i * bw) + (2 * j) + (1 - t) + ((r - 1) * ((1 - t) * 2 - 1)))
in_i = int((i * bw + bw) + (2 * j) + (1 - t))
cl.add(
f"swb_{i}_{j}_cross", "buf", fanin=swb_outs[out_i], fanout=swb_ins[in_i]
)
if r > 2:
# straight
out_i = int(((I * J * 2) - ((2 + i) * bw)) + (2 * j) + t)
in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + t)
cl.add(
f"swb_{i}_{j}_r_straight",
"buf",
fanin=swb_outs[out_i],
fanout=swb_ins[in_i],
)
# cross
out_i = int(
((I * J * 2) - ((2 + i) * bw))
+ (2 * j)
+ (1 - t)
+ ((r - 1) * ((1 - t) * 2 - 1))
)
in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + (1 - t))
cl.add(
f"swb_{i}_{j}_r_cross",
"buf",
fanin=swb_outs[out_i],
fanout=swb_ins[in_i],
)
def full_lock(c, bw, lw, avoid_loops=False):
"""
Locks a circuitgraph with Full-Lock.
Hadi Mardani Kamali, Kimia Zamiri Azar, Houman Homayoun,
and Avesta Sasan. 2019. Full-Lock: Hard Distributions of SAT instances
for Obfuscating Circuits using Fully Configurable Logic and Routing
Blocks. In Proceedings of the 56th Annual Design Automation Conference
2019. Association for Computing Machinery, New York, NY, USA.
Parameters
----------
circuit: circuitgraph.CircuitGraph
Circuit to lock.
banyan_width: int
Width of Banyan network to use, must follow bw = 2**n, n>1.
lut_width: int
Width to use for inserted LUTs, must evenly divide bw.
avoid_loops: bool
If True, gates fed by the Banyan network will be selected
such that they do not cause combinational loops.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# lock with luts
if avoid_loops:
gates = []
potential_gates = {g for g in c.nodes() - c.io() if len(c.fanin(g)) <= lw}
for _ in range(int(bw / lw)):
gates.append(random.choice(list(potential_gates)))
potential_gates -= c.transitive_fanin(gates[-1])
potential_gates -= c.transitive_fanout(gates[-1])
if not potential_gates:
raise ValueError("Could not find enough gates to make " "acyclic lock")
cl, key = random_lut_lock(c, int(bw / lw), lw, gates=gates)
else:
cl, key = random_lut_lock(c, int(bw / lw), lw)
# generate switch
m = cg.tx.strip_io(cg.logic.mux(2))
s = cg.Circuit(name="switch")
s.add_subcircuit(m, "m0")
s.add_subcircuit(m, "m1")
s.add("in_0", "buf", fanout=["m0_in_0", "m1_in_1"])
s.add("in_1", "buf", fanout=["m0_in_1", "m1_in_0"])
s.add("out_0", "xor", fanin="m0_out")
s.add("out_1", "xor", fanin="m1_out")
s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"])
s.add("key_1", "input", fanout="out_0")
s.add("key_2", "input", fanout="out_1")
# generate banyan
I = int(2 * cg.utils.clog2(bw) - 2)
J = int(bw / 2)
# add switches
for i in range(I * J):
cl.add_subcircuit(s, f"swb_{i}")
# make connections
swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)]
swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)]
_connect_banyan(cl, swb_ins, swb_outs, bw)
# get banyan io
net_ins = swb_ins[:bw]
net_outs = swb_outs[-bw:]
# generate key
for i in range(I * J):
for j in range(3):
key[f"swb_{i}_key_{j}"] = choice([True, False])
# get banyan mapping
mapping = {}
polarity = {}
orig_result = cg.sat.solve(cl, {**{n: False for n in net_ins}, **key})
for net_in in net_ins:
result = cg.sat.solve(cl, {**{n: n == net_in for n in net_ins}, **key})
for net_out in net_outs:
if result[net_out] != orig_result[net_out]:
mapping[net_in] = net_out
polarity[net_in] = result[net_out]
break
# connect banyan io to luts
for i in range(int(bw / lw)):
for j in range(lw):
driver = cl.fanin(f"lut_{i}_sel_{j}").pop()
cl.disconnect(driver, f"lut_{i}_sel_{j}")
net_in = net_ins[i * lw + j]
cl.connect(mapping[net_in], f"lut_{i}_sel_{j}")
if not polarity[net_in]:
driver = cl.add(f"not_{net_in}", "not", fanin=driver)
cl.connect(driver, net_in)
for k in key:
cl.set_type(k, "input")
cg.lint(cl)
if avoid_loops and cl.is_cyclic():
raise ValueError("Locked circuit is cyclic")
return cl, key
def full_lock_mux(c, bw, lw):
"""
Locks a circuitgraph with a muxed-based model of Full-Lock.
Uses muxes instead of the Banyan network, a relaxation that breaks symmetry
and simplifies the model substantially.
Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi
Modeling Techniques for Logic Locking. In Proceedings
of the International Conference on Computer Aided Design 2020 (ICCAD-39).
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
banyan_width: int
Width of Banyan network to use, must follow bw = 2**n, n>1.
lut_width: int
Width to use for inserted LUTs, must evenly divide bw.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# first generate banyan, to get a valid mapping for the key
b = cg.Circuit()
# generate switch
m = cg.tx.strip_io(cg.logic.mux(2))
s = cg.Circuit(name="switch")
s.add_subcircuit(m, "m0")
s.add_subcircuit(m, "m1")
s.add("in_0", "buf", fanout=["m0_in_0", "m1_in_1"])
s.add("in_1", "buf", fanout=["m0_in_1", "m1_in_0"])
s.add("out_0", "xor", fanin="m0_out")
s.add("out_1", "xor", fanin="m1_out")
s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"])
s.add("key_1", "input", fanout="out_0")
s.add("key_2", "input", fanout="out_1")
# generate banyan
I = int(2 * cg.utils.clog2(bw) - 2)
J = int(bw / 2)
# add switches
for i in range(I * J):
b.add_subcircuit(s, f"swb_{i}")
# make connections
swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)]
swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)]
_connect_banyan(b, swb_ins, swb_outs, bw)
# get banyan io
net_ins = swb_ins[:bw]
net_outs = swb_outs[-bw:]
# generate key
key = {}
for i in range(I * J):
for j in range(3):
key[f"swb_{i}_key_{j}"] = choice([True, False])
# get banyan mapping
mapping = {}
polarity = {}
orig_result = cg.sat.solve(b, {**{n: False for n in net_ins}, **key})
for net_in in net_ins:
result = cg.sat.solve(
b, {**{n: False if n != net_in else True for n in net_ins}, **key}
)
for net_out in net_outs:
if result[net_out] != orig_result[net_out]:
mapping[net_in] = net_out
polarity[net_in] = result[net_out]
break
# lock with luts
cl, key = random_lut_lock(c, int(bw / lw), lw)
# generate mux
m = cg.tx.strip_io(cg.logic.mux(bw))
# add muxes and xors
banyan_to_mux = {}
for i in range(bw):
cl.add_subcircuit(m, f"mux_{i}")
for b in range(cg.utils.clog2(bw)):
cl.add(f"key_{i}_{b}", "input", fanout=f"mux_{i}_sel_{b}")
cl.add(f"mux_{i}_xor", "xor", fanin=f"mux_{i}_out")
cl.add(f"key_{i}_{cg.utils.clog2(bw)}", "input", fanout=f"mux_{i}_xor")
banyan_to_mux[net_outs[i]] = f"mux_{i}_xor"
# connect muxes to luts
for i in range(bw):
net_in = net_ins[i]
xor = banyan_to_mux[mapping[net_in]]
o = int(xor.split("_")[1])
driver = cl.fanin(f"lut_{i//lw}_sel_{i%lw}").pop()
cl.disconnect(driver, f"lut_{i//lw}_sel_{i%lw}")
if not polarity[net_in]:
driver = cl.add(f"not_{net_in}", "not", fanin=driver)
key[f"key_{o}_{cg.utils.clog2(bw)}"] = True
else:
key[f"key_{o}_{cg.utils.clog2(bw)}"] = False
for b in range(bw):
cl.connect(driver, f"mux_{b}_in_{i}")
cl.connect(xor, f"lut_{i//lw}_sel_{i%lw}")
for b, v in enumerate(cg.utils.int_to_bin(i, cg.utils.clog2(bw), True)):
key[f"key_{o}_{b}"] = v
cg.lint(cl)
return cl, key
def inter_lock(c, bw, reduced_swb=False):
"""
Locks a circuitgraph with InterLock.
Kamali, Hadi Mardani, Kimia Zamiri Azar, Houman Homayoun, and Avesta Sasan.
"Interlock: An intercorrelated logic and routing locking."
In 2020 IEEE/ACM International Conference On Computer Aided Design (ICCAD),
pp. 1-9. IEEE, 2020.
Parameters
----------
circuit: circuitgraph.CircuitGraph
Circuit to lock.
bw: int
The size of the keyed rounting block. A bw of m results
in an m x m sized KeyRB.
reduced_swb: bool
If True, each switchbox is reduced from 3 keys to 1 key due to the fact
that for 100% utilization, the other 2 keys will never be used. Essentially,
the muxes at the output of the switchbox are removed.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
cl = c.copy()
cg.lint(cl)
# generate switch
m = cg.tx.strip_io(cg.logic.mux(2))
s = cg.Circuit(name="switch")
s.add_subcircuit(m, "m0")
s.add_subcircuit(m, "m1")
s.add("in_0", "input", fanout=["m0_in_0", "m1_in_1"])
s.add("in_1", "input", fanout=["m0_in_1", "m1_in_0"])
s.add("ex_in_0", "input")
s.add("ex_in_1", "input")
# f1 and f2 starts as 'and' gates, must be updated later
s.add("f1_out", "and", fanin=["m0_out", "ex_in_0"])
s.add("f2_out", "and", fanin=["m1_out", "ex_in_1"])
s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"])
s.add("out_0", "buf", output=True)
s.add("out_1", "buf", output=True)
if not reduced_swb:
s.add_subcircuit(m, "m2")
s.add_subcircuit(m, "m3")
s.add("key_1", "input", fanout="m2_sel_0")
s.add("key_2", "input", fanout="m3_sel_0")
s.connect("f1_out", "m2_in_0")
s.connect("f2_out", "m3_in_1")
s.connect("m0_out", "m3_in_0")
s.connect("m1_out", "m2_in_1")
s.connect("m2_out", "out_0")
s.connect("m3_out", "out_1")
else:
s.connect("f1_out", "out_0")
s.connect("f2_out", "out_1")
sbb_inputs = ["in_0", "in_1", "ex_in_0", "ex_in_1", "key_0"]
if not reduced_swb:
sbb_inputs += ["key_1", "key_2"]
sbb = cg.BlackBox("switch", sbb_inputs, ["out_0", "out_1"],)
# Select paths to embed in the routing network
path_length = 2 * cg.utils.clog2(bw) - 2
paths = []
filtered_gates = set()
def filter_gate(n):
gate = n
gates = [n]
for _ in range(path_length):
if (
len(cl.fanin(gate)) != 2
or len(cl.fanout(gate)) != 1
or cl.is_output(gate)
or gate in filtered_gates
or len(cl.fanin(gate) & filtered_gates) > 0
or len(cl.fanout(gate) & filtered_gates) > 0
):
return False
gate = cl.fanout(gate).pop()
gates.append(gate)
filtered_gates.update(gates)
for gate in gates:
filtered_gates.update(cl.fanin(gate))
return True
candidate_gates = filter(filter_gate, cl.nodes())
for _ in range(bw):
try:
gate = next(candidate_gates)
except StopIteration as e:
raise ValueError("Not enough candidate gates found for locking") from e
path = [gate]
for _ in range(path_length - 1):
gate = cl.fanout(gate).pop()
path.append(gate)
paths.append(path)
# generate banyan with J rows and I columns of SwBs
I = path_length
J = int(bw / 2)
for i in range(I * J):
cl.add_blackbox(sbb, f"swb_{i}")
# make connections
swb_ins = [f"swb_{i//2}.in_{i%2}" for i in range(I * J * 2)]
swb_outs = [f"swb_{i//2}.out_{i%2}" for i in range(I * J * 2)]
_connect_banyan_bb(cl, swb_ins, swb_outs, bw)
# generate key
# In the example from the paper, the paths in a SWB directly from an
# input to an output are never used. Starting with that implemetation.
# Could sometimes choose paths less than `path_length` and use these
# connections with a decoy external input, but such a strategy is not
# discussed in the paper.
swaps = []
key = {}
for i in range(I * J):
swaps.append(choice([True, False]))
if swaps[-1]:
key[f"swb_{i}_key_0"] = True
else:
key[f"swb_{i}_key_0"] = False
if not reduced_swb:
key[f"swb_{i}_key_1"] = False
key[f"swb_{i}_key_2"] = True
f_gates = {}
# Add paths to banyan
# Get a random intial ordering of paths
input_order = list(range(bw))
shuffle(input_order)
for i, p_idx in enumerate(input_order):
path = paths[p_idx]
swb_idx = i // 2
i_idx = i % 2
prev_node = cl.fanin(path[0]).pop()
cl.connect(prev_node, f"swb_{swb_idx}.in_{i_idx}")
for j, n in enumerate(path):
o_idx = i_idx ^ int(swaps[swb_idx])
ex_i = (cl.fanin(n) - {prev_node}).pop()
cl.connect(ex_i, f"swb_{swb_idx}.ex_in_{o_idx}")
f_gates[f"swb_{swb_idx}_f{o_idx+1}_out"] = cl.type(n)
if j != len(path) - 1:
next_n = cl.fanout(f"swb_{swb_idx}.out_{o_idx}").pop()
next_n = cl.fanout(next_n).pop()
swb_idx = int(next_n.split(".")[0].split("_")[-1])
i_idx = int(next_n.split(".")[-1].split("_")[-1])
prev_node = n
else:
for fo in cl.fanout(n):
cl.disconnect(n, fo)
try:
conn = cl.fanout(f"swb_{swb_idx}.out_{o_idx}").pop()
except KeyError:
conn = cl.add(
f"swb_{swb_idx}_out_{o_idx}_load",
"buf",
fanin=f"swb_{swb_idx}.out_{o_idx}",
)
cl.connect(conn, fo)
for path in paths:
for node in path:
cl.remove(node)
for i in range(I * J):
cl.fill_blackbox(f"swb_{i}", s)
for k, v in f_gates.items():
cl.set_type(k, v)
for k in key:
cl.set_type(k, "input")
cg.lint(cl)
return cl, key
def lebl(c, bw, ng):
"""
Locks a circuitgraph with Logic-Enhanced Banyan Locking.
Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi Modeling Techniques
for Logic Locking. In Proceedings of the International Conference on
Computer Aided Design 2020 (ICCAD-39).
Parameters
----------
c: circuitgraph.CircuitGraph
Circuit to lock.
bw: int
Width of Banyan network.
lw: int
Minimum number of gates mapped to network.
Returns
-------
circuitgraph.CircuitGraph, dict of str:bool
the locked circuit and the correct key value for each key input
"""
# create copy to lock
cl = c.copy()
# generate switch and mux
s = cg.Circuit(name="switch")
m2 = cg.tx.strip_io(cg.logic.mux(2))
s.add_subcircuit(m2, "m2_0")
s.add_subcircuit(m2, "m2_1")
m4 = cg.tx.strip_io(cg.logic.mux(4))
s.add_subcircuit(m4, "m4_0")
s.add_subcircuit(m4, "m4_1")
s.add("in_0", "buf", fanout=["m2_0_in_0", "m2_1_in_1"])
s.add("in_1", "buf", fanout=["m2_0_in_1", "m2_1_in_0"])
s.add("out_0", "buf", fanin="m4_0_out")
s.add("out_1", "buf", fanin="m4_1_out")
s.add("key_0", "input", fanout=["m2_0_sel_0", "m2_1_sel_0"])
s.add("key_1", "input", fanout=["m4_0_sel_0", "m4_1_sel_0"])
s.add("key_2", "input", fanout=["m4_0_sel_1", "m4_1_sel_1"])
# generate banyan
I = int(2 * cg.utils.clog2(bw) - 2)
J = int(bw / 2)
# add switches and muxes
for i in range(I * J):
cl.add_subcircuit(s, f"swb_{i}")
# make connections
swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)]
swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)]
_connect_banyan(cl, swb_ins, swb_outs, bw)
# get banyan io
net_ins = swb_ins[:bw]
net_outs = swb_outs[-bw:]
# generate key
key = {f"swb_{i//3}_key_{i%3}": choice([True, False]) for i in range(3 * I * J)}
# generate connections between banyan nodes
bfi = {n: set() for n in swb_outs + net_ins}
bfo = {n: set() for n in swb_outs + net_ins}
for n in swb_outs + net_ins:
if cl.fanout(n):
fo_node = cl.fanout(n).pop()
swb_i = fo_node.split("_")[1]
bfi[f"swb_{swb_i}_out_0"].add(n)
bfi[f"swb_{swb_i}_out_1"].add(n)
bfo[n].add(f"swb_{swb_i}_out_0")
bfo[n].add(f"swb_{swb_i}_out_1")
# find a mapping of circuit onto banyan
net_map = IDPool()
for bn in swb_outs + net_ins:
for cn in c:
net_map.id(f"m_{bn}_{cn}")
# mapping implications
clauses = []
for bn in swb_outs + net_ins:
# fanin
if bfi[bn]:
for cn in c:
if c.fanin(cn):
for fcn in c.fanin(cn):
clause = [-net_map.id(f"m_{bn}_{cn}")]
clause += [net_map.id(f"m_{fbn}_{fcn}") for fbn in bfi[bn]]
clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfi[bn]]
clauses.append(clause)
else:
clause = [-net_map.id(f"m_{bn}_{cn}")]
clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfi[bn]]
clauses.append(clause)
# fanout
if bfo[bn]:
for cn in c:
clause = [-net_map.id(f"m_{bn}_{cn}")]
clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfo[bn]]
for fcn in c.fanout(cn):
clause += [net_map.id(f"m_{fbn}_{fcn}") for fbn in bfo[bn]]
clauses.append(clause)
# no feed through
for cn in c:
net_map.id(f"INPUT_OR_{cn}")
net_map.id(f"OUTPUT_OR_{cn}")
clauses.append(
[-net_map.id(f"INPUT_OR_{cn}")]
+ [net_map.id(f"m_{bn}_{cn}") for bn in net_ins]
)
clauses.append(
[-net_map.id(f"OUTPUT_OR_{cn}")]
+ [net_map.id(f"m_{bn}_{cn}") for bn in net_outs]
)
for bn in net_ins:
clauses.append([net_map.id(f"INPUT_OR_{cn}"), -net_map.id(f"m_{bn}_{cn}")])
for bn in net_outs:
clauses.append([net_map.id(f"OUTPUT_OR_{cn}"), -net_map.id(f"m_{bn}_{cn}")])
clauses.append([-net_map.id(f"OUTPUT_OR_{cn}"), -net_map.id(f"INPUT_OR_{cn}")])
# at least ngates
for bn in swb_outs + net_ins:
net_map.id(f"NGATES_OR_{bn}")
clauses.append(
[-net_map.id(f"NGATES_OR_{bn}")] + [net_map.id(f"m_{bn}_{cn}") for cn in c]
)
for cn in c:
clauses.append([net_map.id(f"NGATES_OR_{bn}"), -net_map.id(f"m_{bn}_{cn}")])
clauses += CardEnc.atleast(
bound=ng,
lits=[net_map.id(f"NGATES_OR_{bn}") for bn in swb_outs + net_ins],
vpool=net_map,
).clauses
# at most one mapping per out
for bn in swb_outs + net_ins:
clauses += CardEnc.atmost(
lits=[net_map.id(f"m_{bn}_{cn}") for cn in c], vpool=net_map
).clauses
# limit number of times a gate is mapped to net outputs to fanout of gate
for cn in c:
lits = [net_map.id(f"m_{bn}_{cn}") for bn in net_outs]
bound = len(c.fanout(cn))
if len(lits) < bound:
continue
clauses += CardEnc.atmost(bound=bound, lits=lits, vpool=net_map).clauses
# prohibit outputs from net
for bn in swb_outs + net_ins:
for cn in c.outputs():
clauses += [[-net_map.id(f"m_{bn}_{cn}")]]
# solve
solver = Cadical(bootstrap_with=clauses)
if not solver.solve():
raise ValueError(f"No config for width '{bw}'")
model = solver.get_model()
# get mapping
mapping = {}
for bn in swb_outs + net_ins:
selected_gates = [cn for cn in c if model[net_map.id(f"m_{bn}_{cn}") - 1] > 0]
if len(selected_gates) > 1:
raise ValueError(f"Multiple gates mapped to '{bn}'")
mapping[bn] = selected_gates[0] if selected_gates else None
potential_net_fanins = list(
c.nodes()
- (c.endpoints() | set(mapping.values()) | mapping.keys() | c.startpoints())
)
# connect net inputs
for bn in net_ins:
if mapping[bn]:
cl.connect(mapping[bn], bn)
else:
cl.connect(choice(potential_net_fanins), bn)
mapping.update({cl.fanin(bn).pop(): cl.fanin(bn).pop() for bn in net_ins})
potential_net_fanouts = list(
c.nodes()
- (c.startpoints() | set(mapping.values()) | mapping.keys() | c.endpoints())
)
# selected_fo = {}
# connect switch boxes
for i, bn in enumerate(swb_outs):
# get keys
if key[f"swb_{i//2}_key_1"] and key[f"swb_{i//2}_key_2"]:
k = 3
elif not key[f"swb_{i//2}_key_1"] and key[f"swb_{i//2}_key_2"]:
k = 2
elif key[f"swb_{i//2}_key_1"] and not key[f"swb_{i//2}_key_2"]:
k = 1
elif not key[f"swb_{i//2}_key_1"] and not key[f"swb_{i//2}_key_2"]:
k = 0
switch_key = 1 if key[f"swb_{i//2}_key_0"] == 1 else 0
mux_input = f"swb_{i//2}_m4_{i%2}_in_{k}"
# connect inner nodes
mux_gate_types = set()
# constant output, hookup to a node that is already in the affected outputs
# fanin, not in others
if not mapping[bn] and bn in net_outs:
decoy_fanout_gate = choice(potential_net_fanouts)
# selected_fo[bn] = decoy_fanout_gate
if cl.type(decoy_fanout_gate) in ["and", "nand"]:
cl.set_type(mux_input, "1")
elif cl.type(decoy_fanout_gate) in ["or", "nor", "xor", "xnor"]:
cl.set_type(mux_input, "0")
elif cl.type(decoy_fanout_gate) in ["buf"]:
if randint(0, 1):
cl.set_type(mux_input, "1")
cl.set_type(decoy_fanout_gate, choice(["and", "xnor"]))
else:
cl.set_type(mux_input, "0")
cl.set_type(decoy_fanout_gate, choice(["or", "xor"]))
elif cl.type(decoy_fanout_gate) in ["not"]:
if randint(0, 1):
cl.set_type(mux_input, "1")
cl.set_type(decoy_fanout_gate, choice(["nand", "xor"]))
else:
cl.set_type(mux_input, "0")
cl.set_type(decoy_fanout_gate, choice(["nor", "xnor"]))
elif cl.type(decoy_fanout_gate) in ["0", "1"]:
cl.set_type(mux_input, cl.type(decoy_fanout_gate))
cl.set_type(decoy_fanout_gate, "buf")
else:
raise ValueError(f"Invalid gate type '{cl.type(decoy_fanout_gate)}'")
cl.connect(bn, decoy_fanout_gate)
mux_gate_types.add(cl.type(mux_input))
# feedthrough
elif mapping[bn] in [mapping[fbn] for fbn in bfi[bn]]:
cl.set_type(mux_input, "buf")
mux_gate_types.add("buf")
if mapping[cl.fanin(f"swb_{i//2}_in_0").pop()] == mapping[bn]:
cl.connect(f"swb_{i//2}_m2_{switch_key}_out", mux_input)
else:
cl.connect(f"swb_{i//2}_m2_{1-switch_key}_out", mux_input)
# gate
elif mapping[bn]:
cl.set_type(mux_input, cl.type(mapping[bn]))
mux_gate_types.add(cl.type(mapping[bn]))
gfi = cl.fanin(mapping[bn])
if mapping[cl.fanin(f"swb_{i//2}_in_0").pop()] in gfi:
cl.connect(f"swb_{i//2}_m2_{switch_key}_out", mux_input)
gfi.remove(mapping[cl.fanin(f"swb_{i//2}_in_0").pop()])
if mapping[cl.fanin(f"swb_{i//2}_in_1").pop()] in gfi:
cl.connect(f"swb_{i//2}_m2_{1-switch_key}_out", mux_input)
# mapped to None, any key works
else:
k = None
# fill out random gates
for j in range(4):
if j != k:
t = choice(
tuple(
{
"buf",
"or",
"nor",
"and",
"nand",
"not",
"xor",
"xnor",
"0",
"1",
}
- mux_gate_types
)
)
mux_gate_types.add(t)
mux_input = f"swb_{i//2}_m4_{i%2}_in_{j}"
cl.set_type(mux_input, t)
if t in ("not", "buf"):
# pick a random fanin
cl.connect(f"swb_{i//2}_m2_{randint(0,1)}_out", mux_input)
elif t in ("0", "1"):
pass
else:
cl.connect(f"swb_{i//2}_m2_0_out", mux_input)
cl.connect(f"swb_{i//2}_m2_1_out", mux_input)
# connect outputs non constant outs
rev_mapping = {}
for bn in net_outs:
if mapping[bn]:
if mapping[bn] not in rev_mapping:
rev_mapping[mapping[bn]] = set()
rev_mapping[mapping[bn]].add(bn)
for cn, val in rev_mapping.items():
for fcn, bn in zip_longest(cl.fanout(cn), val, fillvalue=list(val)[0]):
cl.connect(bn, fcn)
# delete mapped gates
deleted = True
while deleted:
deleted = False
for n in cl.nodes():
# node and all fanout are in the net
if n not in mapping and n in mapping.values():
if all(
s not in mapping and s in mapping.values() for s in cl.fanout(n)
):
cl.remove(n)
deleted = True
# node in net fanout
if n in [mapping[o] for o in net_outs] and n in cl:
cl.remove(n)
deleted = True
for k in key:
cl.set_type(k, "input")
cg.lint(cl)
return cl, key
Functions
def full_lock(c, bw, lw, avoid_loops=False)
-
Locks a circuitgraph with Full-Lock.
Hadi Mardani Kamali, Kimia Zamiri Azar, Houman Homayoun, and Avesta Sasan. 2019. Full-Lock: Hard Distributions of SAT instances for Obfuscating Circuits using Fully Configurable Logic and Routing Blocks. In Proceedings of the 56th Annual Design Automation Conference 2019. Association for Computing Machinery, New York, NY, USA.
Parameters
circuit
:circuitgraph.CircuitGraph
- Circuit to lock.
banyan_width
:int
- Width of Banyan network to use, must follow bw = 2**n, n>1.
lut_width
:int
- Width to use for inserted LUTs, must evenly divide bw.
avoid_loops
:bool
- If True, gates fed by the Banyan network will be selected such that they do not cause combinational loops.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def full_lock(c, bw, lw, avoid_loops=False): """ Locks a circuitgraph with Full-Lock. Hadi Mardani Kamali, Kimia Zamiri Azar, Houman Homayoun, and Avesta Sasan. 2019. Full-Lock: Hard Distributions of SAT instances for Obfuscating Circuits using Fully Configurable Logic and Routing Blocks. In Proceedings of the 56th Annual Design Automation Conference 2019. Association for Computing Machinery, New York, NY, USA. Parameters ---------- circuit: circuitgraph.CircuitGraph Circuit to lock. banyan_width: int Width of Banyan network to use, must follow bw = 2**n, n>1. lut_width: int Width to use for inserted LUTs, must evenly divide bw. avoid_loops: bool If True, gates fed by the Banyan network will be selected such that they do not cause combinational loops. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # lock with luts if avoid_loops: gates = [] potential_gates = {g for g in c.nodes() - c.io() if len(c.fanin(g)) <= lw} for _ in range(int(bw / lw)): gates.append(random.choice(list(potential_gates))) potential_gates -= c.transitive_fanin(gates[-1]) potential_gates -= c.transitive_fanout(gates[-1]) if not potential_gates: raise ValueError("Could not find enough gates to make " "acyclic lock") cl, key = random_lut_lock(c, int(bw / lw), lw, gates=gates) else: cl, key = random_lut_lock(c, int(bw / lw), lw) # generate switch m = cg.tx.strip_io(cg.logic.mux(2)) s = cg.Circuit(name="switch") s.add_subcircuit(m, "m0") s.add_subcircuit(m, "m1") s.add("in_0", "buf", fanout=["m0_in_0", "m1_in_1"]) s.add("in_1", "buf", fanout=["m0_in_1", "m1_in_0"]) s.add("out_0", "xor", fanin="m0_out") s.add("out_1", "xor", fanin="m1_out") s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"]) s.add("key_1", "input", fanout="out_0") s.add("key_2", "input", fanout="out_1") # generate banyan I = int(2 * cg.utils.clog2(bw) - 2) J = int(bw / 2) # add switches for i in range(I * J): cl.add_subcircuit(s, f"swb_{i}") # make connections swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)] swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)] _connect_banyan(cl, swb_ins, swb_outs, bw) # get banyan io net_ins = swb_ins[:bw] net_outs = swb_outs[-bw:] # generate key for i in range(I * J): for j in range(3): key[f"swb_{i}_key_{j}"] = choice([True, False]) # get banyan mapping mapping = {} polarity = {} orig_result = cg.sat.solve(cl, {**{n: False for n in net_ins}, **key}) for net_in in net_ins: result = cg.sat.solve(cl, {**{n: n == net_in for n in net_ins}, **key}) for net_out in net_outs: if result[net_out] != orig_result[net_out]: mapping[net_in] = net_out polarity[net_in] = result[net_out] break # connect banyan io to luts for i in range(int(bw / lw)): for j in range(lw): driver = cl.fanin(f"lut_{i}_sel_{j}").pop() cl.disconnect(driver, f"lut_{i}_sel_{j}") net_in = net_ins[i * lw + j] cl.connect(mapping[net_in], f"lut_{i}_sel_{j}") if not polarity[net_in]: driver = cl.add(f"not_{net_in}", "not", fanin=driver) cl.connect(driver, net_in) for k in key: cl.set_type(k, "input") cg.lint(cl) if avoid_loops and cl.is_cyclic(): raise ValueError("Locked circuit is cyclic") return cl, key
def full_lock_mux(c, bw, lw)
-
Locks a circuitgraph with a muxed-based model of Full-Lock.
Uses muxes instead of the Banyan network, a relaxation that breaks symmetry and simplifies the model substantially.
Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi Modeling Techniques for Logic Locking. In Proceedings of the International Conference on Computer Aided Design 2020 (ICCAD-39).
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
banyan_width
:int
- Width of Banyan network to use, must follow bw = 2**n, n>1.
lut_width
:int
- Width to use for inserted LUTs, must evenly divide bw.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def full_lock_mux(c, bw, lw): """ Locks a circuitgraph with a muxed-based model of Full-Lock. Uses muxes instead of the Banyan network, a relaxation that breaks symmetry and simplifies the model substantially. Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi Modeling Techniques for Logic Locking. In Proceedings of the International Conference on Computer Aided Design 2020 (ICCAD-39). Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. banyan_width: int Width of Banyan network to use, must follow bw = 2**n, n>1. lut_width: int Width to use for inserted LUTs, must evenly divide bw. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # first generate banyan, to get a valid mapping for the key b = cg.Circuit() # generate switch m = cg.tx.strip_io(cg.logic.mux(2)) s = cg.Circuit(name="switch") s.add_subcircuit(m, "m0") s.add_subcircuit(m, "m1") s.add("in_0", "buf", fanout=["m0_in_0", "m1_in_1"]) s.add("in_1", "buf", fanout=["m0_in_1", "m1_in_0"]) s.add("out_0", "xor", fanin="m0_out") s.add("out_1", "xor", fanin="m1_out") s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"]) s.add("key_1", "input", fanout="out_0") s.add("key_2", "input", fanout="out_1") # generate banyan I = int(2 * cg.utils.clog2(bw) - 2) J = int(bw / 2) # add switches for i in range(I * J): b.add_subcircuit(s, f"swb_{i}") # make connections swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)] swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)] _connect_banyan(b, swb_ins, swb_outs, bw) # get banyan io net_ins = swb_ins[:bw] net_outs = swb_outs[-bw:] # generate key key = {} for i in range(I * J): for j in range(3): key[f"swb_{i}_key_{j}"] = choice([True, False]) # get banyan mapping mapping = {} polarity = {} orig_result = cg.sat.solve(b, {**{n: False for n in net_ins}, **key}) for net_in in net_ins: result = cg.sat.solve( b, {**{n: False if n != net_in else True for n in net_ins}, **key} ) for net_out in net_outs: if result[net_out] != orig_result[net_out]: mapping[net_in] = net_out polarity[net_in] = result[net_out] break # lock with luts cl, key = random_lut_lock(c, int(bw / lw), lw) # generate mux m = cg.tx.strip_io(cg.logic.mux(bw)) # add muxes and xors banyan_to_mux = {} for i in range(bw): cl.add_subcircuit(m, f"mux_{i}") for b in range(cg.utils.clog2(bw)): cl.add(f"key_{i}_{b}", "input", fanout=f"mux_{i}_sel_{b}") cl.add(f"mux_{i}_xor", "xor", fanin=f"mux_{i}_out") cl.add(f"key_{i}_{cg.utils.clog2(bw)}", "input", fanout=f"mux_{i}_xor") banyan_to_mux[net_outs[i]] = f"mux_{i}_xor" # connect muxes to luts for i in range(bw): net_in = net_ins[i] xor = banyan_to_mux[mapping[net_in]] o = int(xor.split("_")[1]) driver = cl.fanin(f"lut_{i//lw}_sel_{i%lw}").pop() cl.disconnect(driver, f"lut_{i//lw}_sel_{i%lw}") if not polarity[net_in]: driver = cl.add(f"not_{net_in}", "not", fanin=driver) key[f"key_{o}_{cg.utils.clog2(bw)}"] = True else: key[f"key_{o}_{cg.utils.clog2(bw)}"] = False for b in range(bw): cl.connect(driver, f"mux_{b}_in_{i}") cl.connect(xor, f"lut_{i//lw}_sel_{i%lw}") for b, v in enumerate(cg.utils.int_to_bin(i, cg.utils.clog2(bw), True)): key[f"key_{o}_{b}"] = v cg.lint(cl) return cl, key
def inter_lock(c, bw, reduced_swb=False)
-
Locks a circuitgraph with InterLock.
Kamali, Hadi Mardani, Kimia Zamiri Azar, Houman Homayoun, and Avesta Sasan. "Interlock: An intercorrelated logic and routing locking." In 2020 IEEE/ACM International Conference On Computer Aided Design (ICCAD), pp. 1-9. IEEE, 2020.
Parameters
circuit
:circuitgraph.CircuitGraph
- Circuit to lock.
bw
:int
- The size of the keyed rounting block. A bw of m results in an m x m sized KeyRB.
reduced_swb
:bool
- If True, each switchbox is reduced from 3 keys to 1 key due to the fact that for 100% utilization, the other 2 keys will never be used. Essentially, the muxes at the output of the switchbox are removed.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def inter_lock(c, bw, reduced_swb=False): """ Locks a circuitgraph with InterLock. Kamali, Hadi Mardani, Kimia Zamiri Azar, Houman Homayoun, and Avesta Sasan. "Interlock: An intercorrelated logic and routing locking." In 2020 IEEE/ACM International Conference On Computer Aided Design (ICCAD), pp. 1-9. IEEE, 2020. Parameters ---------- circuit: circuitgraph.CircuitGraph Circuit to lock. bw: int The size of the keyed rounting block. A bw of m results in an m x m sized KeyRB. reduced_swb: bool If True, each switchbox is reduced from 3 keys to 1 key due to the fact that for 100% utilization, the other 2 keys will never be used. Essentially, the muxes at the output of the switchbox are removed. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ cl = c.copy() cg.lint(cl) # generate switch m = cg.tx.strip_io(cg.logic.mux(2)) s = cg.Circuit(name="switch") s.add_subcircuit(m, "m0") s.add_subcircuit(m, "m1") s.add("in_0", "input", fanout=["m0_in_0", "m1_in_1"]) s.add("in_1", "input", fanout=["m0_in_1", "m1_in_0"]) s.add("ex_in_0", "input") s.add("ex_in_1", "input") # f1 and f2 starts as 'and' gates, must be updated later s.add("f1_out", "and", fanin=["m0_out", "ex_in_0"]) s.add("f2_out", "and", fanin=["m1_out", "ex_in_1"]) s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"]) s.add("out_0", "buf", output=True) s.add("out_1", "buf", output=True) if not reduced_swb: s.add_subcircuit(m, "m2") s.add_subcircuit(m, "m3") s.add("key_1", "input", fanout="m2_sel_0") s.add("key_2", "input", fanout="m3_sel_0") s.connect("f1_out", "m2_in_0") s.connect("f2_out", "m3_in_1") s.connect("m0_out", "m3_in_0") s.connect("m1_out", "m2_in_1") s.connect("m2_out", "out_0") s.connect("m3_out", "out_1") else: s.connect("f1_out", "out_0") s.connect("f2_out", "out_1") sbb_inputs = ["in_0", "in_1", "ex_in_0", "ex_in_1", "key_0"] if not reduced_swb: sbb_inputs += ["key_1", "key_2"] sbb = cg.BlackBox("switch", sbb_inputs, ["out_0", "out_1"],) # Select paths to embed in the routing network path_length = 2 * cg.utils.clog2(bw) - 2 paths = [] filtered_gates = set() def filter_gate(n): gate = n gates = [n] for _ in range(path_length): if ( len(cl.fanin(gate)) != 2 or len(cl.fanout(gate)) != 1 or cl.is_output(gate) or gate in filtered_gates or len(cl.fanin(gate) & filtered_gates) > 0 or len(cl.fanout(gate) & filtered_gates) > 0 ): return False gate = cl.fanout(gate).pop() gates.append(gate) filtered_gates.update(gates) for gate in gates: filtered_gates.update(cl.fanin(gate)) return True candidate_gates = filter(filter_gate, cl.nodes()) for _ in range(bw): try: gate = next(candidate_gates) except StopIteration as e: raise ValueError("Not enough candidate gates found for locking") from e path = [gate] for _ in range(path_length - 1): gate = cl.fanout(gate).pop() path.append(gate) paths.append(path) # generate banyan with J rows and I columns of SwBs I = path_length J = int(bw / 2) for i in range(I * J): cl.add_blackbox(sbb, f"swb_{i}") # make connections swb_ins = [f"swb_{i//2}.in_{i%2}" for i in range(I * J * 2)] swb_outs = [f"swb_{i//2}.out_{i%2}" for i in range(I * J * 2)] _connect_banyan_bb(cl, swb_ins, swb_outs, bw) # generate key # In the example from the paper, the paths in a SWB directly from an # input to an output are never used. Starting with that implemetation. # Could sometimes choose paths less than `path_length` and use these # connections with a decoy external input, but such a strategy is not # discussed in the paper. swaps = [] key = {} for i in range(I * J): swaps.append(choice([True, False])) if swaps[-1]: key[f"swb_{i}_key_0"] = True else: key[f"swb_{i}_key_0"] = False if not reduced_swb: key[f"swb_{i}_key_1"] = False key[f"swb_{i}_key_2"] = True f_gates = {} # Add paths to banyan # Get a random intial ordering of paths input_order = list(range(bw)) shuffle(input_order) for i, p_idx in enumerate(input_order): path = paths[p_idx] swb_idx = i // 2 i_idx = i % 2 prev_node = cl.fanin(path[0]).pop() cl.connect(prev_node, f"swb_{swb_idx}.in_{i_idx}") for j, n in enumerate(path): o_idx = i_idx ^ int(swaps[swb_idx]) ex_i = (cl.fanin(n) - {prev_node}).pop() cl.connect(ex_i, f"swb_{swb_idx}.ex_in_{o_idx}") f_gates[f"swb_{swb_idx}_f{o_idx+1}_out"] = cl.type(n) if j != len(path) - 1: next_n = cl.fanout(f"swb_{swb_idx}.out_{o_idx}").pop() next_n = cl.fanout(next_n).pop() swb_idx = int(next_n.split(".")[0].split("_")[-1]) i_idx = int(next_n.split(".")[-1].split("_")[-1]) prev_node = n else: for fo in cl.fanout(n): cl.disconnect(n, fo) try: conn = cl.fanout(f"swb_{swb_idx}.out_{o_idx}").pop() except KeyError: conn = cl.add( f"swb_{swb_idx}_out_{o_idx}_load", "buf", fanin=f"swb_{swb_idx}.out_{o_idx}", ) cl.connect(conn, fo) for path in paths: for node in path: cl.remove(node) for i in range(I * J): cl.fill_blackbox(f"swb_{i}", s) for k, v in f_gates.items(): cl.set_type(k, v) for k in key: cl.set_type(k, "input") cg.lint(cl) return cl, key
def lebl(c, bw, ng)
-
Locks a circuitgraph with Logic-Enhanced Banyan Locking.
Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi Modeling Techniques for Logic Locking. In Proceedings of the International Conference on Computer Aided Design 2020 (ICCAD-39).
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
bw
:int
- Width of Banyan network.
lw
:int
- Minimum number of gates mapped to network.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def lebl(c, bw, ng): """ Locks a circuitgraph with Logic-Enhanced Banyan Locking. Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi Modeling Techniques for Logic Locking. In Proceedings of the International Conference on Computer Aided Design 2020 (ICCAD-39). Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. bw: int Width of Banyan network. lw: int Minimum number of gates mapped to network. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() # generate switch and mux s = cg.Circuit(name="switch") m2 = cg.tx.strip_io(cg.logic.mux(2)) s.add_subcircuit(m2, "m2_0") s.add_subcircuit(m2, "m2_1") m4 = cg.tx.strip_io(cg.logic.mux(4)) s.add_subcircuit(m4, "m4_0") s.add_subcircuit(m4, "m4_1") s.add("in_0", "buf", fanout=["m2_0_in_0", "m2_1_in_1"]) s.add("in_1", "buf", fanout=["m2_0_in_1", "m2_1_in_0"]) s.add("out_0", "buf", fanin="m4_0_out") s.add("out_1", "buf", fanin="m4_1_out") s.add("key_0", "input", fanout=["m2_0_sel_0", "m2_1_sel_0"]) s.add("key_1", "input", fanout=["m4_0_sel_0", "m4_1_sel_0"]) s.add("key_2", "input", fanout=["m4_0_sel_1", "m4_1_sel_1"]) # generate banyan I = int(2 * cg.utils.clog2(bw) - 2) J = int(bw / 2) # add switches and muxes for i in range(I * J): cl.add_subcircuit(s, f"swb_{i}") # make connections swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)] swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)] _connect_banyan(cl, swb_ins, swb_outs, bw) # get banyan io net_ins = swb_ins[:bw] net_outs = swb_outs[-bw:] # generate key key = {f"swb_{i//3}_key_{i%3}": choice([True, False]) for i in range(3 * I * J)} # generate connections between banyan nodes bfi = {n: set() for n in swb_outs + net_ins} bfo = {n: set() for n in swb_outs + net_ins} for n in swb_outs + net_ins: if cl.fanout(n): fo_node = cl.fanout(n).pop() swb_i = fo_node.split("_")[1] bfi[f"swb_{swb_i}_out_0"].add(n) bfi[f"swb_{swb_i}_out_1"].add(n) bfo[n].add(f"swb_{swb_i}_out_0") bfo[n].add(f"swb_{swb_i}_out_1") # find a mapping of circuit onto banyan net_map = IDPool() for bn in swb_outs + net_ins: for cn in c: net_map.id(f"m_{bn}_{cn}") # mapping implications clauses = [] for bn in swb_outs + net_ins: # fanin if bfi[bn]: for cn in c: if c.fanin(cn): for fcn in c.fanin(cn): clause = [-net_map.id(f"m_{bn}_{cn}")] clause += [net_map.id(f"m_{fbn}_{fcn}") for fbn in bfi[bn]] clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfi[bn]] clauses.append(clause) else: clause = [-net_map.id(f"m_{bn}_{cn}")] clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfi[bn]] clauses.append(clause) # fanout if bfo[bn]: for cn in c: clause = [-net_map.id(f"m_{bn}_{cn}")] clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfo[bn]] for fcn in c.fanout(cn): clause += [net_map.id(f"m_{fbn}_{fcn}") for fbn in bfo[bn]] clauses.append(clause) # no feed through for cn in c: net_map.id(f"INPUT_OR_{cn}") net_map.id(f"OUTPUT_OR_{cn}") clauses.append( [-net_map.id(f"INPUT_OR_{cn}")] + [net_map.id(f"m_{bn}_{cn}") for bn in net_ins] ) clauses.append( [-net_map.id(f"OUTPUT_OR_{cn}")] + [net_map.id(f"m_{bn}_{cn}") for bn in net_outs] ) for bn in net_ins: clauses.append([net_map.id(f"INPUT_OR_{cn}"), -net_map.id(f"m_{bn}_{cn}")]) for bn in net_outs: clauses.append([net_map.id(f"OUTPUT_OR_{cn}"), -net_map.id(f"m_{bn}_{cn}")]) clauses.append([-net_map.id(f"OUTPUT_OR_{cn}"), -net_map.id(f"INPUT_OR_{cn}")]) # at least ngates for bn in swb_outs + net_ins: net_map.id(f"NGATES_OR_{bn}") clauses.append( [-net_map.id(f"NGATES_OR_{bn}")] + [net_map.id(f"m_{bn}_{cn}") for cn in c] ) for cn in c: clauses.append([net_map.id(f"NGATES_OR_{bn}"), -net_map.id(f"m_{bn}_{cn}")]) clauses += CardEnc.atleast( bound=ng, lits=[net_map.id(f"NGATES_OR_{bn}") for bn in swb_outs + net_ins], vpool=net_map, ).clauses # at most one mapping per out for bn in swb_outs + net_ins: clauses += CardEnc.atmost( lits=[net_map.id(f"m_{bn}_{cn}") for cn in c], vpool=net_map ).clauses # limit number of times a gate is mapped to net outputs to fanout of gate for cn in c: lits = [net_map.id(f"m_{bn}_{cn}") for bn in net_outs] bound = len(c.fanout(cn)) if len(lits) < bound: continue clauses += CardEnc.atmost(bound=bound, lits=lits, vpool=net_map).clauses # prohibit outputs from net for bn in swb_outs + net_ins: for cn in c.outputs(): clauses += [[-net_map.id(f"m_{bn}_{cn}")]] # solve solver = Cadical(bootstrap_with=clauses) if not solver.solve(): raise ValueError(f"No config for width '{bw}'") model = solver.get_model() # get mapping mapping = {} for bn in swb_outs + net_ins: selected_gates = [cn for cn in c if model[net_map.id(f"m_{bn}_{cn}") - 1] > 0] if len(selected_gates) > 1: raise ValueError(f"Multiple gates mapped to '{bn}'") mapping[bn] = selected_gates[0] if selected_gates else None potential_net_fanins = list( c.nodes() - (c.endpoints() | set(mapping.values()) | mapping.keys() | c.startpoints()) ) # connect net inputs for bn in net_ins: if mapping[bn]: cl.connect(mapping[bn], bn) else: cl.connect(choice(potential_net_fanins), bn) mapping.update({cl.fanin(bn).pop(): cl.fanin(bn).pop() for bn in net_ins}) potential_net_fanouts = list( c.nodes() - (c.startpoints() | set(mapping.values()) | mapping.keys() | c.endpoints()) ) # selected_fo = {} # connect switch boxes for i, bn in enumerate(swb_outs): # get keys if key[f"swb_{i//2}_key_1"] and key[f"swb_{i//2}_key_2"]: k = 3 elif not key[f"swb_{i//2}_key_1"] and key[f"swb_{i//2}_key_2"]: k = 2 elif key[f"swb_{i//2}_key_1"] and not key[f"swb_{i//2}_key_2"]: k = 1 elif not key[f"swb_{i//2}_key_1"] and not key[f"swb_{i//2}_key_2"]: k = 0 switch_key = 1 if key[f"swb_{i//2}_key_0"] == 1 else 0 mux_input = f"swb_{i//2}_m4_{i%2}_in_{k}" # connect inner nodes mux_gate_types = set() # constant output, hookup to a node that is already in the affected outputs # fanin, not in others if not mapping[bn] and bn in net_outs: decoy_fanout_gate = choice(potential_net_fanouts) # selected_fo[bn] = decoy_fanout_gate if cl.type(decoy_fanout_gate) in ["and", "nand"]: cl.set_type(mux_input, "1") elif cl.type(decoy_fanout_gate) in ["or", "nor", "xor", "xnor"]: cl.set_type(mux_input, "0") elif cl.type(decoy_fanout_gate) in ["buf"]: if randint(0, 1): cl.set_type(mux_input, "1") cl.set_type(decoy_fanout_gate, choice(["and", "xnor"])) else: cl.set_type(mux_input, "0") cl.set_type(decoy_fanout_gate, choice(["or", "xor"])) elif cl.type(decoy_fanout_gate) in ["not"]: if randint(0, 1): cl.set_type(mux_input, "1") cl.set_type(decoy_fanout_gate, choice(["nand", "xor"])) else: cl.set_type(mux_input, "0") cl.set_type(decoy_fanout_gate, choice(["nor", "xnor"])) elif cl.type(decoy_fanout_gate) in ["0", "1"]: cl.set_type(mux_input, cl.type(decoy_fanout_gate)) cl.set_type(decoy_fanout_gate, "buf") else: raise ValueError(f"Invalid gate type '{cl.type(decoy_fanout_gate)}'") cl.connect(bn, decoy_fanout_gate) mux_gate_types.add(cl.type(mux_input)) # feedthrough elif mapping[bn] in [mapping[fbn] for fbn in bfi[bn]]: cl.set_type(mux_input, "buf") mux_gate_types.add("buf") if mapping[cl.fanin(f"swb_{i//2}_in_0").pop()] == mapping[bn]: cl.connect(f"swb_{i//2}_m2_{switch_key}_out", mux_input) else: cl.connect(f"swb_{i//2}_m2_{1-switch_key}_out", mux_input) # gate elif mapping[bn]: cl.set_type(mux_input, cl.type(mapping[bn])) mux_gate_types.add(cl.type(mapping[bn])) gfi = cl.fanin(mapping[bn]) if mapping[cl.fanin(f"swb_{i//2}_in_0").pop()] in gfi: cl.connect(f"swb_{i//2}_m2_{switch_key}_out", mux_input) gfi.remove(mapping[cl.fanin(f"swb_{i//2}_in_0").pop()]) if mapping[cl.fanin(f"swb_{i//2}_in_1").pop()] in gfi: cl.connect(f"swb_{i//2}_m2_{1-switch_key}_out", mux_input) # mapped to None, any key works else: k = None # fill out random gates for j in range(4): if j != k: t = choice( tuple( { "buf", "or", "nor", "and", "nand", "not", "xor", "xnor", "0", "1", } - mux_gate_types ) ) mux_gate_types.add(t) mux_input = f"swb_{i//2}_m4_{i%2}_in_{j}" cl.set_type(mux_input, t) if t in ("not", "buf"): # pick a random fanin cl.connect(f"swb_{i//2}_m2_{randint(0,1)}_out", mux_input) elif t in ("0", "1"): pass else: cl.connect(f"swb_{i//2}_m2_0_out", mux_input) cl.connect(f"swb_{i//2}_m2_1_out", mux_input) # connect outputs non constant outs rev_mapping = {} for bn in net_outs: if mapping[bn]: if mapping[bn] not in rev_mapping: rev_mapping[mapping[bn]] = set() rev_mapping[mapping[bn]].add(bn) for cn, val in rev_mapping.items(): for fcn, bn in zip_longest(cl.fanout(cn), val, fillvalue=list(val)[0]): cl.connect(bn, fcn) # delete mapped gates deleted = True while deleted: deleted = False for n in cl.nodes(): # node and all fanout are in the net if n not in mapping and n in mapping.values(): if all( s not in mapping and s in mapping.values() for s in cl.fanout(n) ): cl.remove(n) deleted = True # node in net fanout if n in [mapping[o] for o in net_outs] and n in cl: cl.remove(n) deleted = True for k in key: cl.set_type(k, "input") cg.lint(cl) return cl, key
def lut_lock(c, num_gates, count_keys=False, skip_fi1=True, rank_by_shared_fanin=False, key_prefix='key_')
-
Locks a circuitgraph with NB2-MO-HSC LUT-lock.
H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan, "LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410.
Parameters
circuit
:circuitgraph.CircuitGraph
- Circuit to lock.
num_gates
:int
- The number of gates to lock.
count_keys
:bool
- If true, continue locking until at least
num_gates
keys are added instead ofnum_gates
gates. skip_fi1
:int
- If True, nodes with a fanin of 1 (i.e. buf or inv) will not be considered for locking.
rank_by_shared_fanin
:bool
- If True, the output with the least shared fanin with other outputs will be selected for locking first. By default, the output with the least amount of total fanin is selected for locking first.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Raises
ValueError
- if there are not enough viable gates to lock.
Expand source code
def lut_lock( c, num_gates, count_keys=False, skip_fi1=True, rank_by_shared_fanin=False, key_prefix="key_", ): """ Locks a circuitgraph with NB2-MO-HSC LUT-lock. H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan, "LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410. Parameters ---------- circuit: circuitgraph.CircuitGraph Circuit to lock. num_gates: int The number of gates to lock. count_keys: bool If true, continue locking until at least `num_gates` keys are added instead of `num_gates` gates. skip_fi1: int If True, nodes with a fanin of 1 (i.e. buf or inv) will not be considered for locking. rank_by_shared_fanin: bool If True, the output with the least shared fanin with other outputs will be selected for locking first. By default, the output with the least amount of total fanin is selected for locking first. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input Raises ------ ValueError if there are not enough viable gates to lock. """ # create copy to lock cl = c.copy() def calc_skew(gate, cl): d = {False: 0, True: 0} fanin = list(cl.fanin(gate)) # create subcircuit containing just gate for simulation simc = cg.Circuit() for i in fanin: simc.add(i, "input") simc.add(gate, cl.type(gate), fanin=fanin) # simulate for i, vs in enumerate(product([False, True], repeat=len(fanin))): assumptions = dict(zip(fanin, vs[::-1])) result = cg.sat.solve(simc, assumptions) if not result: d[False] += 1 else: d[result[gate]] += 1 num_combos = 2 ** len(fanin) return abs(d[False] / num_combos - d[True] / num_combos) def replace_lut(gate, cl): key = {} m = cg.logic.mux(2 ** len(cl.fanin(gate))) fanout = list(cl.fanout(gate)) fanin = list(cl.fanin(gate)) # create LUT cl.add_subcircuit(m, f"lut_{gate}") # create subcircuit containing just gate for simulation simc = cg.Circuit() for i in fanin: simc.add(i, "input") simc.add(gate, cl.type(gate), fanin=fanin) # connect keys for i, vs in enumerate(product([False, True], repeat=len(fanin))): assumptions = dict(zip(fanin, vs[::-1])) cl.add(f"{key_prefix}{gate}_{i}", "input", fanout=f"lut_{gate}_in_{i}") result = cg.sat.solve(simc, assumptions) if not result: key[f"{key_prefix}{gate}_{i}"] = False else: key[f"{key_prefix}{gate}_{i}"] = result[gate] # connect out cl.disconnect(gate, fanout) cl.connect(f"lut_{gate}_out", fanout) # connect sel for i, f in enumerate(fanin): cl.connect(f, f"lut_{gate}_sel_{i}") # delete gate cl.remove(gate) return key, [f"lut_{gate}_{n}" for n in m.nodes()], f"lut_{gate}_out" def continue_locking(locked_gates, num_gates, keys, count_keys): if count_keys: return len(keys) < num_gates return locked_gates < num_gates locked_gates = 0 outputs = list(cl.outputs()) if rank_by_shared_fanin: def rank_output(x): other_outputs = [o for o in outputs if o != x] other_fanin = cl.transitive_fanin(other_outputs) curr_fanin = cl.transitive_fanin(x) return len(curr_fanin & other_fanin) else: def rank_output(x): return len(cl.transitive_fanin(x)) outputs.sort(key=rank_output) candidates = [] forbidden_nodes = set() keys = {} while continue_locking(locked_gates, num_gates, keys, count_keys): if not candidates: outputs = [o for o in outputs if o not in forbidden_nodes] try: candidates.append(outputs.pop(0)) except IndexError as e: raise ValueError( "Ran out of candidate gates at " f"{locked_gates} gates." ) from e else: candidate = candidates.pop(0) candidate_is_output = cl.is_output(candidate) children = cl.fanin(candidate) if candidate in forbidden_nodes: candidates += [g for g in children if g not in forbidden_nodes] continue forbidden_nodes.add(candidate) if len(children) == 0: continue if skip_fi1 and len(children) == 1: child = children.pop() if child not in forbidden_nodes | set(candidates): candidates.insert(0, child) continue key, nodes, output_to_relabel = replace_lut(candidate, cl) keys.update(key) forbidden_nodes.update(nodes) cl = cg.tx.relabel(cl, {output_to_relabel: candidate}) if candidate_is_output: cl.set_output(candidate) for g1 in children: forbidden_nodes.add(g1) for g2 in cl.fanin(g1): if g2 not in forbidden_nodes | set(candidates): candidates.append(g2) # Sort by least number of outputs in fanout cone, then most skew candidates.sort( key=lambda x: ( len(cl.transitive_fanout(x) & cl.outputs()), -calc_skew(x, cl), ) ) locked_gates += 1 return cl, keys
def mux_lock(c, keylen, avoid_loops=False, key_prefix='key_')
-
Locks a circuitgraph with a mux lock.
J. Rajendran et al., "Fault Analysis-Based Logic Encryption," in IEEE Transactions on Computers, vol. 64, no. 2, pp. 410-424, Feb. 2015, doi: 10.1109/TC.2013.193.
Note that a random mux selection is used, not fault-based.
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
keylen
:int
- the number of bits in the key.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def mux_lock(c, keylen, avoid_loops=False, key_prefix="key_"): """ Locks a circuitgraph with a mux lock. J. Rajendran et al., "Fault Analysis-Based Logic Encryption," in IEEE Transactions on Computers, vol. 64, no. 2, pp. 410-424, Feb. 2015, doi: 10.1109/TC.2013.193. Note that a random mux selection is used, not fault-based. Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. keylen: int the number of bits in the key. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() # get 2:1 mux m = cg.logic.mux(2) # randomly select gates gates = sample(tuple(cl.nodes() - cl.outputs()), keylen) if avoid_loops: decoy_gates = set() else: decoy_gates = sample(tuple(cl.nodes() - cl.outputs()), keylen) # insert key gates key = {} for i, gate in enumerate(gates): # select random key value key_val = choice([True, False]) if avoid_loops: decoy_gate = choice( tuple( c.nodes() - c.io() - set(gates) - cl.transitive_fanout(gate) - decoy_gates ) ) decoy_gates.add(decoy_gate) else: decoy_gate = decoy_gates[i] # create and connect mux fanout = cl.fanout(gate) cl.disconnect(gate, fanout) cl.add_subcircuit(m, f"mux_{i}") cl.connect(f"mux_{i}_out", fanout) key_in = cl.add(f"{key_prefix}{i}", "input", fanout=f"mux_{i}_sel_0", uid=True) key[key_in] = key_val if key_val: cl.connect(gate, f"mux_{i}_in_1") cl.connect(decoy_gate, f"mux_{i}_in_0") else: cl.connect(gate, f"mux_{i}_in_0") cl.connect(decoy_gate, f"mux_{i}_in_1") cg.lint(cl) if avoid_loops and cl.is_cyclic(): raise ValueError("Locked circuit is cyclic") return cl, key
def random_lut_lock(c, num_gates, lut_width, gates=None)
-
Locks a circuitgraph by replacing random gates with LUTs.
This is kind of like applying LUT-lock with no replacement strategy. (H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan, "LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410.)
Parameters
circuit
:circuitgraph.CircuitGraph
- Circuit to lock.
num_gates
:int
- the number of gates to lock.
lut_width
:int
- LUT width, defines maximum fanin of locked gates.
gates
:list
ofstr
- The gates to lock. If not provided, will be randomly sampled
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def random_lut_lock(c, num_gates, lut_width, gates=None): """ Locks a circuitgraph by replacing random gates with LUTs. This is kind of like applying LUT-lock with no replacement strategy. (H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan, "LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410.) Parameters ---------- circuit: circuitgraph.CircuitGraph Circuit to lock. num_gates: int the number of gates to lock. lut_width: int LUT width, defines maximum fanin of locked gates. gates: list of str The gates to lock. If not provided, will be randomly sampled Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() # parse mux m = cg.logic.mux(2 ** lut_width) # randomly select gates potential_gates = {g for g in cl.nodes() - cl.io() if len(cl.fanin(g)) <= lut_width} if gates: if len(gates) != num_gates: raise ValueError( f"Got 'num_gates' of {num_gates} but length of " f"'gates' is {len(gates)}" ) if any(len(cl.fanin(g)) > lut_width for g in gates): raise ValueError("cannot lock a gate with fanin greater than " "lut_width") if any(g in cl.io() for g in gates): raise ValueError("cannot lock an input/output gate") else: gates = sample(tuple(potential_gates), num_gates) potential_gates -= set(gates) potential_gates -= cl.transitive_fanout(gates) # insert key gates key = {} for i, gate in enumerate(gates): fanout = list(cl.fanout(gate)) fanin = list(cl.fanin(gate)) try: padding = sample( tuple(potential_gates - cl.fanin(gate)), lut_width - len(fanin) ) except ValueError as e: raise ValueError("Could not find enough viable gates for padding") from e # create LUT cl.add_subcircuit(m, f"lut_{i}") # connect keys for j, vs in enumerate(product([False, True], repeat=len(fanin + padding))): assumptions = { s: v for s, v in zip(fanin + padding, vs[::-1]) if s in fanin } key_in = cl.add( f"key_{i*2**lut_width+j}", "input", fanout=f"lut_{i}_in_{j}", uid=True ) result = cg.sat.solve(c, assumptions) if not result: key[key_in] = False else: key[key_in] = result[gate] # connect out cl.disconnect(gate, fanout) cl.connect(f"lut_{i}_out", fanout) # connect sel for j, f in enumerate(fanin + padding): cl.connect(f, f"lut_{i}_sel_{j}") # delete gate cl.remove(gate) cl = cg.tx.relabel(cl, {f"lut_{i}_out": gate}) cg.lint(cl) return cl, key
def sfll_flex(c, width, n, target_output=None)
-
Locks a circuitgraph with SFLL-flex.
Note that in this implementation the original circuit is not functionally stripped, meaning that it does not produce an inverted response for the protected input pattern. This makes this implementation vulnurable to removal attacks. However, it can still be used to measure SAT attack resiliance.
Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel, Mohammed Ashraf, Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017. Provably-Secure Logic Locking: From Theory To Practice. In Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security. Association for Computing Machinery, New York, NY, USA, 1601–1618.
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
width
:int
- the minimum fanin of the gates to lock.
n
:int
- number of input patterns to lock.
target_output
:str
- If defined, this output will be the one which is locked. Otherwise, a random output will be locked.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def sfll_flex(c, width, n, target_output=None): """ Locks a circuitgraph with SFLL-flex. Note that in this implementation the original circuit is not functionally stripped, meaning that it does not produce an inverted response for the protected input pattern. This makes this implementation vulnurable to removal attacks. However, it can still be used to measure SAT attack resiliance. Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel, Mohammed Ashraf, Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017. Provably-Secure Logic Locking: From Theory To Practice. In Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security. Association for Computing Machinery, New York, NY, USA, 1601–1618. Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. width: int the minimum fanin of the gates to lock. n: int number of input patterns to lock. target_output: str If defined, this output will be the one which is locked. Otherwise, a random output will be locked. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() if not target_output: target_output = random.choice(list(cl.outputs())) # get inputs to lock target_inputs = cl.startpoints(target_output) if len(target_inputs) < width: target_inputs |= set( random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs)) ) target_inputs = list(target_inputs) # create key key = {f"key_{i}": choice([True, False]) for i in range(width * n)} # connect comparators cl.add("flip_out", "or") cl.add("restore_out", "or") for j in range(n): cl.add(f"flip_and_{j}", "and", fanout="flip_out") cl.add(f"restore_and_{j}", "and", fanout="restore_out") for i, inp in enumerate(random.sample(target_inputs, width)): for j in range(n): cl.add(f"key_{i+j*width}", "input") cl.add(f"hardcoded_key_{i}_{j}", "1" if key[f"key_{i+j*width}"] else "0") cl.add( f"restore_xor_{i}_{j}", "xor", fanin=[f"key_{i+j*width}", inp], fanout=f"restore_and_{j}", ) cl.add( f"flip_xor_{i}_{j}", "xor", fanin=[f"hardcoded_key_{i}_{j}", inp], fanout=f"flip_and_{j}", ) # flip output old_out = cl.uid(f"{target_output}_pre_lock") cl = cg.tx.relabel(cl, {target_output: old_out}) cl.set_output(old_out, False) cl.add( target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True ) cg.lint(cl) return cl, key
def sfll_hd(c, width, hd, target_output=None)
-
Locks a circuitgraph with SFLL-HD.
Note that in this implementation the original circuit is not functionally stripped, meaning that it does not produce an inverted response for the protected input pattern. This makes this implementation vulnurable to removal attacks. However, it can still be used to measure SAT attack resiliance.
Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel, Mohammed Ashraf, Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017. Provably-Secure Logic Locking: From Theory To Practice. In Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security (CCS ’17). Association for Computing Machinery, New York, NY, USA, 1601–1618.
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
width
:int
- key width, also the minimum fanin of the gates to lock.
hd
:int
- the hamming distance to lock with, as explained in the paper.
target_output
:str
- If defined, this output will be the one which is locked. Otherwise, a random output will be locked.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def sfll_hd(c, width, hd, target_output=None): """ Locks a circuitgraph with SFLL-HD. Note that in this implementation the original circuit is not functionally stripped, meaning that it does not produce an inverted response for the protected input pattern. This makes this implementation vulnurable to removal attacks. However, it can still be used to measure SAT attack resiliance. Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel, Mohammed Ashraf, Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017. Provably-Secure Logic Locking: From Theory To Practice. In Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security (CCS ’17). Association for Computing Machinery, New York, NY, USA, 1601–1618. Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. width: int key width, also the minimum fanin of the gates to lock. hd: int the hamming distance to lock with, as explained in the paper. target_output: str If defined, this output will be the one which is locked. Otherwise, a random output will be locked. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() # parse popcount p = cg.logic.popcount(width) if len(c.inputs()) < width: raise ValueError(f"Not enough inputs to lock with width '{width}'") if not target_output: target_output = random.choice(list(cl.outputs())) # get inputs to lock target_inputs = cl.startpoints(target_output) if len(target_inputs) < width: target_inputs |= set( random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs)) ) target_inputs = list(target_inputs) # create key key = {f"key_{i}": random.choice([True, False]) for i in range(width)} # instantiate and connect hd circuits cl.add_subcircuit(p, "flip_pop") cl.add_subcircuit(p, "restore_pop") # connect inputs for i, inp in enumerate(random.sample(target_inputs, width)): cl.add(f"key_{i}", "input") cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0") cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp]) cl.add(f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp]) cl.connect(f"flip_xor_{i}", f"flip_pop_in_{i}") cl.connect(f"restore_xor_{i}", f"restore_pop_in_{i}") # connect outputs cl.add("flip_out", "and") cl.add("restore_out", "and") for i, v in enumerate(format(hd, f"0{cg.utils.clog2(width)+1}b")[::-1]): cl.add(f"hd_{i}", v) cl.add( f"restore_out_xnor_{i}", "xnor", fanin=[f"hd_{i}", f"restore_pop_out_{i}"], fanout="restore_out", ) cl.add( f"flip_out_xnor_{i}", "xnor", fanin=[f"hd_{i}", f"flip_pop_out_{i}"], fanout="flip_out", ) # flip output old_out = cl.uid(f"{target_output}_pre_lock") cl = cg.tx.relabel(cl, {target_output: old_out}) cl.set_output(old_out, False) cl.add( target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True ) cg.lint(cl) return cl, key
def trll(c, keylen, s1_s2_ratio=1, shuffle_key=True, seed=None)
-
Locks a circuitgraph with Truly Random Logic Locking.
Limaye, E. Kalligeros, N. Karousos, I. G. Karybali and O. Sinanoglu, "Thwarting All Logic Locking Attacks: Dishonest Oracle With Truly Random Logic Locking," in IEEE Transactions on Computer-Aided Design of Integrated Circuits and Systems, vol. 40, no. 9, pp. 1740-1753, Sept. 2021.
Parameters
c
:circuitgraph.Circuit
- The circuit to lock.
keylen
:int
- The number of key bits to add.
s1_s2_ratio
:int
orstr
- The ratio between number of key gate locations locked where an inverter exists in the original design (s1) or where an inverter does not exist in the original design (s2). The paper leaves this value at 1 (meaning s1=s2=keylen/2), but they note that this could be adjusted based on the ratio of the locations where there is an inverter in the original netlist. Setting this parameter to the string "infer" will do this adjustment. I.e. s1 = keylenr, s2 = keylen(1-r), where r is the number of inverters in the circuit divided by the total number of gates.
shuffle_key
:bool
- By default, the key input labels are shuffled at the end of the algorithm so the labelling does not reveal which portion of the algorithm the key input was added during.
seed
:int
- Seed for the random selection of gates and shuffling of the key.
Returns
circuitgraph.Circuit, dict
ofstr:bool
- The locked circuit and the correct key value for each key input.
Expand source code
def trll(c, keylen, s1_s2_ratio=1, shuffle_key=True, seed=None): """ Locks a circuitgraph with Truly Random Logic Locking. Limaye, E. Kalligeros, N. Karousos, I. G. Karybali and O. Sinanoglu, "Thwarting All Logic Locking Attacks: Dishonest Oracle With Truly Random Logic Locking," in IEEE Transactions on Computer-Aided Design of Integrated Circuits and Systems, vol. 40, no. 9, pp. 1740-1753, Sept. 2021. Parameters ---------- c: circuitgraph.Circuit The circuit to lock. keylen: int The number of key bits to add. s1_s2_ratio: int or str The ratio between number of key gate locations locked where an inverter exists in the original design (s1) or where an inverter does not exist in the original design (s2). The paper leaves this value at 1 (meaning s1=s2=keylen/2), but they note that this could be adjusted based on the ratio of the locations where there is an inverter in the original netlist. Setting this parameter to the string "infer" will do this adjustment. I.e. s1 = keylen*r, s2 = keylen*(1-r), where r is the number of inverters in the circuit divided by the total number of gates. shuffle_key: bool By default, the key input labels are shuffled at the end of the algorithm so the labelling does not reveal which portion of the algorithm the key input was added during. seed: int Seed for the random selection of gates and shuffling of the key. Returns ------- circuitgraph.Circuit, dict of str:bool The locked circuit and the correct key value for each key input. """ rng = random.Random(seed) cl = c.copy() if keylen % 2 != 0: raise NotImplementedError if s1_s2_ratio == "infer": raise NotImplementedError s1 = int((keylen // 2) * s1_s2_ratio) if s1 > keylen: raise ValueError(f"Unusable s1_s2_ratio: {s1_s2_ratio}") s2 = keylen - s1 s1a = rng.randint(0, s1) s1b = s1 - s1a s2a = rng.randint(0, s2) s2b = s2 - s2a inv_gates = list(c.filter_type("not")) rng.shuffle(inv_gates) rem_gates = list( c.nodes() - c.io() - c.filter_type(("not", "bb_input", "bb_output", "0", "1", "x")) ) rng.shuffle(rem_gates) j = 0 k = {} # Replace existing inv_gates with XOR key-gates for _ in range(s1a): sel_gate = inv_gates.pop() ki = f"key_{j}" cl.add(ki, "input") k[ki] = True cl.set_type(sel_gate, "xor") cl.connect(ki, sel_gate) j += 1 # Add XOR key-gates before existing inv_gates for _ in range(s1b): sel_gate = inv_gates.pop() ki = f"key_{j}" cl.add(ki, "input") k[ki] = False inv_fanin = cl.fanin(sel_gate) assert len(inv_fanin) == 1 cl.disconnect(inv_fanin, sel_gate) cl.add(f"key_gate_{j}", "xor", fanin=inv_fanin | {ki}, fanout=sel_gate) j += 1 # Add XOR key-gates and INV gates after existing rem_gates for _ in range(s2a): sel_gate = rem_gates.pop() ki = f"key_{j}" cl.add(ki, "input") k[ki] = True sel_fanout = cl.fanout(sel_gate) cl.disconnect(sel_gate, sel_fanout) cl.add(f"key_gate_{j}", "xor", fanin=(sel_gate, ki)) cl.add(f"key_inv_{j}", "not", fanin=f"key_gate_{j}", fanout=sel_fanout) j += 1 # Add XOR key-gates after existing rem_gates for _ in range(s2b): sel_gate = rem_gates.pop() ki = f"key_{j}" cl.add(ki, "input") k[ki] = False sel_fanout = cl.fanout(sel_gate) cl.disconnect(sel_gate, sel_fanout) cl.add(f"key_gate_{j}", "xor", fanin=(sel_gate, ki), fanout=sel_fanout) j += 1 # Shuffle keys if shuffle_key: new_order = list(range(keylen)) rng.shuffle(new_order) shuffled_k = {} intermediate_mapping = {} final_mapping = {} for old_idx, new_idx in enumerate(new_order): shuffled_k[f"key_{new_idx}"] = k[f"key_{old_idx}"] intermediate_mapping[f"key_{old_idx}"] = f"key_{old_idx}_temp" final_mapping[f"key_{old_idx}_temp"] = f"key_{new_idx}" cl.relabel(intermediate_mapping) cl.relabel(final_mapping) return cl, shuffled_k return cl, k
def tt_lock(c, width, target_output=None)
-
Locks a circuitgraph with TTLock.
Note that in this implementation the original circuit is not functionally stripped, meaning that it does not produce an inverted response for the protected input pattern. This makes this implementation vulnurable to removal attacks. However, it can still be used to measure SAT attack resiliance.
M. Yasin, A. Sengupta, B. Schafer, Y. Makris, O. Sinanoglu, and J. Rajendran, “What to Lock?: Functional and Parametric Locking,” in Great Lakes Symposium on VLSI, pp. 351–356, 2017.
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
width
:int
- the minimum fanin of the gates to lock.
target_output
:str
- If defined, this output will be the one which is locked. Otherwise, a random output will be locked.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def tt_lock(c, width, target_output=None): """ Locks a circuitgraph with TTLock. Note that in this implementation the original circuit is not functionally stripped, meaning that it does not produce an inverted response for the protected input pattern. This makes this implementation vulnurable to removal attacks. However, it can still be used to measure SAT attack resiliance. M. Yasin, A. Sengupta, B. Schafer, Y. Makris, O. Sinanoglu, and J. Rajendran, “What to Lock?: Functional and Parametric Locking,” in Great Lakes Symposium on VLSI, pp. 351–356, 2017. Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. width: int the minimum fanin of the gates to lock. target_output: str If defined, this output will be the one which is locked. Otherwise, a random output will be locked. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() if len(c.inputs()) < width: raise ValueError(f"Not enough inputs to lock with width '{width}'") if not target_output: target_output = random.choice(list(cl.outputs())) # get inputs to lock target_inputs = cl.startpoints(target_output) if len(target_inputs) < width: target_inputs |= set( random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs)) ) target_inputs = list(target_inputs) # create key key = {f"key_{i}": random.choice([True, False]) for i in range(width)} # connect comparators cl.add("flip_out", "and") cl.add("restore_out", "and") for i, inp in enumerate(random.sample(target_inputs, width)): cl.add(f"key_{i}", "input") cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0") cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp], fanout="restore_out") cl.add( f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp], fanout="flip_out" ) # flip output old_out = cl.uid(f"{target_output}_pre_lock") cl = cg.tx.relabel(cl, {target_output: old_out}) cl.set_output(old_out, False) cl.add( target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True ) cg.lint(cl) return cl, key
def tt_lock_sen(c, width, nsamples=10)
-
Locks a circuitgraph with TTLock-Sen.
Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi, “Sensitivity Analysis of Locked Circuits,” in Logic for Programming, Artificial Intelligence and Reasoning (LPAR-23), pp. 483-497. EPiC Series in Computing 73, EasyChair.
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
width
:int
- the minimum fanin of the gates to lock.
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def tt_lock_sen(c, width, nsamples=10): """ Locks a circuitgraph with TTLock-Sen. Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi, “Sensitivity Analysis of Locked Circuits,” in Logic for Programming, Artificial Intelligence and Reasoning (LPAR-23), pp. 483-497. EPiC Series in Computing 73, EasyChair. Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. width: int the minimum fanin of the gates to lock. Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() # find output with large enough fanin potential_outs = [o for o in cl.outputs() if len(cl.startpoints(o)) >= width] if not potential_outs: raise ValueError(f"Not enough inputs to lock with width '{width}'") # find average sensitivities A = {} N = {} S = {} for o in potential_outs: # build sensitivity circuit s = cg.tx.sensitivity_transform(c, o) startpoints = c.startpoints(o) s_out = {o for o in s.outputs() if "difference" in o} # est avg sensitivity total = 0 for i in range(nsamples): input_val = {i: randint(0, 1) for i in startpoints} model = cg.sat.solve(s, input_val) sen = sum(model[o] for o in s_out) total += sen A[o] = int(total / nsamples) N[o] = len(startpoints) S[o] = s # find output + input value with closest to avg sen def find_input(): b = 0 while b < max(N.values()): for o in potential_outs: upper = min(N[o], int(N[o] - A[o] + b)) lower = max(0, int(N[o] - A[o] - b)) us = cg.utils.int_to_bin(upper, cg.utils.clog2(N[o])) ls = cg.utils.int_to_bin(lower, cg.utils.clog2(N[o])) for sv in [us, ls]: model = cg.sat.solve( S[o], {f"sen_out_{i}": v for i, v in enumerate(sv)} ) if model: out = o startpoints = c.startpoints(o) key = {f"key_{i}": model[n] for i, n in enumerate(startpoints)} return key, startpoints, out b += 1 key, startpoints, out = find_input() # connect comparators cl.add("flip_out", "and") cl.add("restore_out", "and") for i, inp in enumerate(startpoints): cl.add(f"key_{i}", "input") cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0") cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp], fanout="restore_out") cl.add( f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp], fanout="flip_out" ) # flip output old_out = cl.uid(f"{out}_pre_lock") cl = cg.tx.relabel(cl, {out: old_out}) cl.set_output(old_out, False) cl.add(out, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True) cg.lint(cl) return cl, key
def xor_lock(c, keylen, key_prefix='key_', replacement=False)
-
Locks a circuitgraph with a random xor lock.
J. A. Roy, F. Koushanfar and I. L. Markov, "Ending Piracy of Integrated Circuits," in Computer, vol. 43, no. 10, pp. 30-38, Oct. 2010.
Parameters
c
:circuitgraph.CircuitGraph
- Circuit to lock.
keylen
:int
- the number of bits in the key
replacement
:bool
- If True, the same line can be locked twice (resulting in a chain of key gates)
Returns
circuitgraph.CircuitGraph, dict
ofstr:bool
- the locked circuit and the correct key value for each key input
Expand source code
def xor_lock(c, keylen, key_prefix="key_", replacement=False): """ Locks a circuitgraph with a random xor lock. J. A. Roy, F. Koushanfar and I. L. Markov, "Ending Piracy of Integrated Circuits," in Computer, vol. 43, no. 10, pp. 30-38, Oct. 2010. Parameters ---------- c: circuitgraph.CircuitGraph Circuit to lock. keylen: int the number of bits in the key replacement: bool If True, the same line can be locked twice (resulting in a chain of key gates) Returns ------- circuitgraph.CircuitGraph, dict of str:bool the locked circuit and the correct key value for each key input """ # create copy to lock cl = c.copy() # randomly select gates to lock if replacement: gates = choices(tuple(cl.nodes() - cl.outputs()), k=keylen) else: gates = sample(tuple(cl.nodes() - cl.outputs()), keylen) # insert key gates key = {} for i, gate in enumerate(gates): # select random key value key[f"{key_prefix}{i}"] = choice([True, False]) # create xor/xnor,input gate_type = "xnor" if key[f"{key_prefix}{i}"] else "xor" fanout = cl.fanout(gate) cl.disconnect(gate, fanout) cl.add(f"key_gate_{i}", gate_type, fanin=gate, fanout=fanout) cl.add(f"{key_prefix}{i}", "input", fanout=f"key_gate_{i}") cg.lint(cl) return cl, key