This tutorial covers:
wbkit
), existing and customwbkit
)Figure credits: Wang et al. On Stability of Multi-Valued Nonlinear Feedback Shift Registers.
from circkit.boolean import OptBooleanCircuit as BooleanCircuit
from wbkit.prng import NFSR, Pool
C = BooleanCircuit(name="prng")
nfsr = NFSR(
# f(x) = 1 ^ x0 ^ x2 ^ x1&x2&x3
taps=[[], [0], [2], [1, 2, 3]],
clocks_initial=10,
clocks_per_step=2,
)
x = C.add_inputs(4, "x%d")
nfsr.set_state(x)
for i in range(5):
rand = nfsr.step()
C.add_output(rand)
C.in_place_remove_unused_nodes()
from binteger import Bin
for i in range(2**4):
x = Bin(i, 4).tuple
print(x, "->", C.evaluate(x))
(0, 0, 0, 0) -> [0, 1, 0, 0, 1] (0, 0, 0, 1) -> [1, 0, 0, 1, 0] (0, 0, 1, 0) -> [0, 1, 0, 0, 1] (0, 0, 1, 1) -> [1, 0, 0, 1, 0] (0, 1, 0, 0) -> [0, 0, 1, 0, 0] (0, 1, 0, 1) -> [1, 1, 1, 0, 0] (0, 1, 1, 0) -> [0, 0, 1, 0, 0] (0, 1, 1, 1) -> [1, 1, 0, 0, 1] (1, 0, 0, 0) -> [0, 1, 0, 0, 1] (1, 0, 0, 1) -> [1, 0, 0, 1, 0] (1, 0, 1, 0) -> [0, 0, 1, 1, 1] (1, 0, 1, 1) -> [0, 1, 1, 1, 1] (1, 1, 0, 0) -> [0, 0, 1, 0, 0] (1, 1, 0, 1) -> [1, 1, 1, 1, 0] (1, 1, 1, 0) -> [1, 0, 0, 1, 1] (1, 1, 1, 1) -> [1, 1, 1, 1, 1]
C.digraph().view()
'Digraph.gv.pdf'
At the cost of sacrificing provably security guarantees, we can reuse NFSR outputs many times, in order to improve efficiency.
from circkit.boolean import OptBooleanCircuit as BooleanCircuit
from wbkit.prng import NFSR, Pool
C = BooleanCircuit(name="prng")
nfsr = NFSR(
# f(x) = 1 ^ x0 ^ x2 ^ x1&x2&x3
taps=[[], [0], [2], [1, 2, 3]],
clocks_initial=10,
clocks_per_step=2,
)
prng = Pool(prng=nfsr, n=5)
x = C.add_inputs(4, "x%d")
prng.set_state(x)
for i in range(10):
rand = prng.step()
C.add_output(rand)
C.in_place_remove_unused_nodes()
from binteger import Bin
for i in range(2**4):
x = Bin(i, 4).tuple
print(x, "->", C.evaluate(x))
(0, 0, 0, 0) -> [1, 0, 0, 1, 1, 0, 0, 0, 0, 0] (0, 0, 0, 1) -> [0, 0, 1, 0, 0, 1, 1, 0, 1, 1] (0, 0, 1, 0) -> [1, 0, 0, 1, 1, 0, 0, 0, 0, 0] (0, 0, 1, 1) -> [0, 0, 1, 0, 0, 1, 1, 0, 1, 1] (0, 1, 0, 0) -> [0, 1, 0, 0, 0, 0, 0, 1, 0, 0] (0, 1, 0, 1) -> [0, 1, 1, 0, 1, 0, 0, 1, 0, 0] (0, 1, 1, 0) -> [0, 1, 0, 0, 0, 0, 0, 1, 0, 0] (0, 1, 1, 1) -> [1, 0, 1, 1, 1, 0, 0, 0, 0, 0] (1, 0, 0, 0) -> [1, 0, 0, 1, 1, 0, 0, 0, 0, 0] (1, 0, 0, 1) -> [0, 0, 1, 0, 0, 1, 1, 0, 1, 1] (1, 0, 1, 0) -> [1, 1, 0, 1, 0, 1, 1, 1, 1, 1] (1, 0, 1, 1) -> [1, 1, 0, 1, 1, 1, 1, 1, 1, 1] (1, 1, 0, 0) -> [0, 1, 0, 0, 0, 0, 0, 1, 0, 0] (1, 1, 0, 1) -> [0, 1, 1, 0, 1, 1, 1, 1, 1, 1] (1, 1, 1, 0) -> [1, 0, 1, 1, 0, 1, 1, 0, 1, 1] (1, 1, 1, 1) -> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Setup AES Circuit:
from binteger import Bin
from circkit.boolean import OptBooleanCircuit as BooleanCircuit
from wbkit.ciphers.aes import BitAES
C = BooleanCircuit(name="AES")
key = b"abcdefghABCDEFGH"
plaintext = b"0123456789abcdef"
pt = C.add_inputs(128)
ct, k10 = BitAES(pt, Bin(key).tuple, rounds=10)
C.add_output(ct)
C.in_place_remove_unused_nodes()
C.print_stats()
ct = C.evaluate(Bin(plaintext).tuple)
AES(OptBooleanCircuit): | 128 inputs, 128 outputs, 31273 nodes | XOR:19284 (61.66%), AND:6240 (19.95%), NOT:5621 (17.97%), INPUT:128 (0.41%)
Setup better PRNG:
nfsr = NFSR(
taps=[[], [11], [50], [3, 107]],
clocks_initial=100,
clocks_per_step=1,
)
prng = Pool(prng=nfsr, n=256)
Statically: Linear masking splits each intermediate computed value $s$ into $n$ linear shares: $$s = x_1 \oplus x_2 \oplus \ldots \oplus x_n$$
Dynamically: Replace original operations with gadgets operating on shares of the inputs and computing shares of the output in a secure way. Requires randomness for encoding inputs / gadgets.
Security: against correlation of any $\le n-1$ shares/intermediates to the original sensitive value $s$.
from wbkit.masking import ISW
C_ISW = ISW(prng=prng, order=1).transform(C)
C_ISW.in_place_remove_unused_nodes()
C_ISW.print_stats()
assert ct == C_ISW.evaluate(Bin(plaintext).tuple)
AES_ISW(OptBooleanCircuit): | 128 inputs, 128 outputs, 95392 nodes | XOR:64496 (67.61%), AND:24836 (26.04%), NOT:5932 (6.22%), INPUT:128 (0.13%)
Statically: Split each intermediate computed value $s$ into $3$ (non)linear shares: $$s = x_1x_2 \oplus x_3$$
Dynamically: Similar to linear (computations using gadgets).
Security: against linear algebraic (linear decoding) attacks.
from wbkit.masking import MINQ
C_MINQ = MINQ(prng=prng).transform(C)
C_MINQ.in_place_remove_unused_nodes()
C_MINQ.print_stats()
assert ct == C_MINQ.evaluate(Bin(plaintext).tuple)
AES_MINQ(OptBooleanCircuit): | 128 inputs, 128 outputs, 837645 nodes | XOR:577583 (68.95%), AND:253957 (30.32%), NOT:5977 (0.71%), INPUT:128 (0.02%)
Statically: Split each intermediate computed value $s$ into linear and non-linear shares:
$$s = \tilde{x_1} \tilde{x_2} \oplus x_1 \oplus \ldots \oplus x_n$$(more nonlinear shares are possible (provable construction for 2,3 shares))
Dynamically: Similar to linear (computations using gadgets).
Security: against linear algebraic (linear decoding) attacks.
from wbkit.masking import QuadLin
C_QL = QuadLin(prng=prng, n_linear=3).transform(C)
C_QL.in_place_remove_unused_nodes()
C_QL.print_stats()
assert ct == C_QL.evaluate(Bin(plaintext).tuple)
AES_QuadLin(OptBooleanCircuit): | 128 inputs, 128 outputs,1471640 nodes | XOR:1073933 (72.98%), AND:391602 (26.61%), NOT:5977 (0.41%), INPUT:128 (0.01%)
Idea: duplicate the circuit $C$ identically $n$ times (slots), choose dynamically at random the position of the right input, compute on random inputs in the other slots.
Statically: Each variable is shared into $n$ shares
$$(x_1, \ldots, x_n), ~\text{where}~ x_i = s ~~\text{if}~i = f(\text{input})~~\text{and random otherwise}$$Dynamically: Perform usual computations independently on each of the slots (+shuffle inputs and determine the right output).
Security: against the linear algebraic attack.
from wbkit.masking import DumShuf
C_DS = DumShuf(prng=prng, n_shares=2).transform(C)
C_DS.in_place_remove_unused_nodes()
C_DS.print_stats()
assert ct == C_DS.evaluate(Bin(plaintext).tuple)
AES_DumShuf(OptBooleanCircuit): | 128 inputs, 128 outputs, 77729 nodes | XOR:52656 (67.74%), AND:13347 (17.17%), NOT:11598 (14.92%), INPUT:128 (0.16%)
To do fast trace recording, we serialize the circuit into an internal binary format, into a file:
from wbkit.serialize import RawSerializer
RawSerializer().serialize_to_file(C, "circuits/aes10_clear.bin")
Note: the serializer does not support constants (only the NOT operation), so the call to
C.in_place_remove_unused_nodes()
is necessary.
Generate circuits of the 2-round AES protected using the following countermeasure combinations and save them to respective files:
circuits/aes2_clear.bin
: unprotected AEScircuits/aes2_isw2.bin
: linear masking (ISW) with 2 shares (order 1)circuits/aes2_isw3.bin
: linear masking (ISW) with 3 shares (order 2)circuits/aes2_minq.bin
: minimalist quadratic masking (~same as combined with 1 linear share)circuits/aes2_quadlin2.bin
: combined quadratic masking (2 linear shares)circuits/aes2_quadlin3.bin
: combined quadratic masking (3 linear shares)Note: you can use the existing prng
object.
SOLUTION BELOW, WILL BE REMOVED
from binteger import Bin
from circkit.boolean import OptBooleanCircuit as BooleanCircuit
from wbkit.ciphers.aes import BitAES
C = BooleanCircuit(name="AES")
key = b"abcdefghABCDEFGH"
plaintext = b"0123456789abcdef"
pt = C.add_inputs(128)
ct, k10 = BitAES(pt, Bin(key).tuple, rounds=2)
C.add_output(ct)
C.in_place_remove_unused_nodes()
import gc
from wbkit.masking import QuadLin, ISW, MINQ, DumShuf
isw2 = ISW(prng=prng, order=1)
isw3 = ISW(prng=prng, order=2)
minq = MINQ(prng=prng)
quadlin1 = QuadLin(prng=prng, n_linear=1)
quadlin2 = QuadLin(prng=prng, n_linear=2)
quadlin3 = QuadLin(prng=prng, n_linear=3)
dumshuf2 = DumShuf(prng=prng, n_shares=2)
dumshuf3 = DumShuf(prng=prng, n_shares=3)
todo = {
"circuits/aes2_clear.bin": [],
"circuits/aes2_isw2.bin": [isw2],
"circuits/aes2_isw3.bin": [isw3],
"circuits/aes2_minq.bin": [minq],
"circuits/aes2_quadlin1.bin": [quadlin1],
"circuits/aes2_quadlin2.bin": [quadlin2],
"circuits/aes2_quadlin3.bin": [quadlin3],
"circuits/aes2_dumshuf2.bin": [dumshuf2],
"circuits/aes2_dumshuf3.bin": [dumshuf3],
}
for name, trs in todo.items():
print(name)
Cnew = C
for tr in trs:
Cnew = tr.transform(Cnew)
Cnew.in_place_remove_unused_nodes()
Cnew.print_stats()
RawSerializer().serialize_to_file(Cnew, name)
print()
gc.collect()
circuits/aes2_clear.bin AES(OptBooleanCircuit): | 128 inputs, 128 outputs, 5825 nodes | XOR:3380 (58.03%), AND:1248 (21.42%), NOT:1069 (18.35%), INPUT:128 (2.20%) circuits/aes2_isw2.bin AES_ISW(OptBooleanCircuit): | 128 inputs, 128 outputs, 19487 nodes | XOR:12720 (65.27%), AND:5252 (26.95%), NOT:1387 (7.12%), INPUT:128 (0.66%) circuits/aes2_isw3.bin AES_ISW(OptBooleanCircuit): | 128 inputs, 128 outputs, 39056 nodes | XOR:26339 (67.44%), AND:11204 (28.69%), NOT:1385 (3.55%), INPUT:128 (0.33%) circuits/aes2_minq.bin AES_MINQ(OptBooleanCircuit): | 128 inputs, 128 outputs, 162928 nodes | XOR:113795 (69.84%), AND:47580 (29.20%), NOT:1425 (0.87%), INPUT:128 (0.08%) circuits/aes2_quadlin1.bin AES_QuadLin(OptBooleanCircuit): | 128 inputs, 128 outputs, 163520 nodes | XOR:114377 (69.95%), AND:47590 (29.10%), NOT:1425 (0.87%), INPUT:128 (0.08%) circuits/aes2_quadlin2.bin AES_QuadLin(OptBooleanCircuit): | 128 inputs, 128 outputs, 210575 nodes | XOR:148937 (70.73%), AND:60085 (28.53%), NOT:1425 (0.68%), INPUT:128 (0.06%) circuits/aes2_quadlin3.bin AES_QuadLin(OptBooleanCircuit): | 128 inputs, 128 outputs, 284619 nodes | XOR:208042 (73.09%), AND:75024 (26.36%), NOT:1425 (0.50%), INPUT:128 (0.04%) circuits/aes2_dumshuf2.bin AES_DumShuf(OptBooleanCircuit): | 128 inputs, 128 outputs, 16849 nodes | XOR:10864 (64.48%), AND:3363 (19.96%), NOT:2494 (14.80%), INPUT:128 (0.76%) circuits/aes2_dumshuf3.bin AES_DumShuf(OptBooleanCircuit): | 128 inputs, 128 outputs, 35593 nodes | XOR:24309 (68.30%), AND:7593 (21.33%), NOT:3563 (10.01%), INPUT:128 (0.36%)