Skip to content

Commit 380c203

Browse files
[otbnsim] Cycle-accurate Python model of Trivium/Bivium
This commit introduces a cycle-accurate Python implementation of the Trivium primitive (see `prim_trivium.sv`) for the eventual replacement of the OTBN PRNG. Signed-off-by: Andrea Caforio <andrea.caforio@lowrisc.org>
1 parent 697c06a commit 380c203

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
#!/usr/bin/env python3
2+
import math
3+
from enum import IntEnum
4+
5+
6+
class SeedType(IntEnum):
7+
# This is the regular (standardized for Trivium) method for seeding the
8+
# cipher whereby an 80-bit key and 80-bit IV are injected into the state
9+
# before the initialization rounds are executed.
10+
KEY_IV = 0
11+
# The entire state is seeded once with a seed of the same length. No
12+
# initialization rounds are executed.
13+
STATE_FULL = 1
14+
# Every seed operation fills a chunk of predefined size of the state
15+
# starting with the least significant region until every bit of the state
16+
# has been seeded. The seed operations can be interspersed with update
17+
# invocations such that keystream and seeding can take place concurrently.
18+
STATE_PARTIAL = 2
19+
20+
21+
class CipherType(IntEnum):
22+
# Both Trivium and its simpler variant Bivium can be instantiated. Note
23+
# only Trivium is a standardized cipher while Bivium serves a vehicule to
24+
# study the cryptanalytic properties of this family of ciphers. Both
25+
# primitives can be used to instantiate a PRNG.
26+
TRIVIUM = 0
27+
BIVIUM = 1
28+
29+
30+
def i2b(i: int, n: int) -> list[int]:
31+
"""Convert a little endian integer to a bit array with the LSB at idx 0.
32+
The resulting bit array is padded with 0s if log2(i) < `n` until its size
33+
is `n` bits."""
34+
return [int(b) for b in bin(i)[2:].zfill(n)][::-1]
35+
36+
37+
def b2i(b: list[int]) -> int:
38+
"""Convert a bit array with the LSB at idx 0 to a little endian integer."""
39+
return int("".join(str(d) for d in b[::-1]), 2)
40+
41+
42+
class Trivium:
43+
"""This is a cycle-accurate model of the OpenTitan Trivium primitive.
44+
45+
Instantiating this class corresponds to the cipher state after the reset.
46+
Subsequently, two operations can be scheduled in a clock interval, i.e.,
47+
between calls to `clock`.
48+
49+
- `seed`: Pass a seed to the cipher that will appear in the state after
50+
the next `clock` call. Depending on the seed type different update
51+
sequences are necessary to complete the initialization routines.
52+
53+
- KEY_IV: The entire state needs to be updated 4 times over which
54+
means ceil(4 * `STATE_SIZE` / `OUTPUT_WIDTH`) calls to `update`
55+
and `clock`.
56+
- STATE_FULL: Having called `clock` after `seed` immediately readies
57+
the cipher for the generation of keystream bits.
58+
- STATE_PARTIAL: ceil(`STATE_SIZE` / `PART_SEED_SIZE`) clock
59+
intervals with `seed` are required before the cipher is ready.
60+
There can intervals without `seed` calls. This models stall in
61+
the generation of seed bits from entropy complex.
62+
63+
- `update`: Run the state update function and generate an update state
64+
that replaces the current state at the end of the clock interval,
65+
i.e., after the `clock` call.
66+
"""
67+
68+
TRIVIUM_STATE_SIZE = 288
69+
BIVIUM_STATE_SIZE = 177
70+
71+
PART_SEED_SIZE = 32
72+
TRIVIUM_LAST_PART_SEED_SIZE = 32
73+
BIVIUM_LAST_PART_SEED_SIZE = 17
74+
75+
# Initial state after reset (see `prim_trivium_pkg.sv`).
76+
TRIVIUM_INIT_SEED = i2b(
77+
0x758A442031E1C4616EA343EC153282A30C132B5723C5A4CF4743B3C7C32D580F74F1713A, 288
78+
)
79+
BIVIUM_INIT_SEED = TRIVIUM_INIT_SEED[0:BIVIUM_STATE_SIZE]
80+
81+
def __init__(self, cipher_type: CipherType, seed_type: SeedType, output_width: int):
82+
"""The cipher is defined by its cipher type (see `CipherType`), its
83+
seed type (see `SeedType`) and the output width."""
84+
85+
if cipher_type == CipherType.TRIVIUM:
86+
self.state_size = self.TRIVIUM_STATE_SIZE
87+
self.state = self.TRIVIUM_INIT_SEED[:]
88+
self.update_func = self.trivium_update
89+
self.last_part_seed_size = self.TRIVIUM_LAST_PART_SEED_SIZE
90+
elif cipher_type == CipherType.BIVIUM:
91+
self.state_size = self.BIVIUM_STATE_SIZE
92+
self.state = self.BIVIUM_INIT_SEED[:]
93+
self.update_func = self.bivium_update
94+
self.last_part_seed_size = self.BIVIUM_LAST_PART_SEED_SIZE
95+
else:
96+
raise ValueError("unknown cipher type:", cipher_type)
97+
98+
# Depending on cipher and seed type, a different number of seed rounds
99+
# have to be run.
100+
if seed_type == SeedType.KEY_IV:
101+
self.seed_rnd = math.ceil(4 * self.state_size / output_width)
102+
self.seed_ctr = 0
103+
elif seed_type == SeedType.STATE_FULL:
104+
self.seed_rnd = 1
105+
self.seed_ctr = 0
106+
elif seed_type == SeedType.STATE_PARTIAL:
107+
self.seed_rnd = math.ceil(self.state_size / self.PART_SEED_SIZE)
108+
self.seed_ctr = 0
109+
else:
110+
raise ValueError("unknown seed type:", seed_type)
111+
112+
self.cipher_type = cipher_type
113+
self.seed_type = seed_type
114+
self.output_width = output_width
115+
116+
# Scheduled state and seed for the current clock interval.
117+
self.next_state = []
118+
self.next_seed = []
119+
120+
self.ks = [0] * output_width
121+
122+
def update(self) -> None:
123+
"""Run the state update function `OUTPUT_WIDTH`-many times
124+
and schedule the new state to replace the current state at
125+
end of the clock interval."""
126+
127+
if self.next_state != []:
128+
raise Exception("cannot update more than once per clock interval")
129+
130+
self.next_state = self.state[:]
131+
for i in range(self.output_width):
132+
self.ks[i] = self.update_func(self.next_state)
133+
134+
def seed(self, seed) -> None:
135+
"""Schedule a new seed that depending on the seed type will
136+
be injected into the state at the end of the clock
137+
interval."""
138+
139+
if self.seed_type == SeedType.KEY_IV:
140+
assert len(seed) == 160
141+
142+
key = seed[0:80]
143+
iv = seed[80:160]
144+
145+
if self.cipher_type == CipherType.TRIVIUM:
146+
self.next_seed = (
147+
(key + [0] * 13) + (iv + [0] * 4) + ([0] * 108 + [1, 1, 1])
148+
)
149+
else:
150+
self.next_seed = (key + [0] * 13) + (iv + [0] * 4)
151+
152+
elif self.seed_type == SeedType.STATE_FULL:
153+
assert len(seed) == self.state_size
154+
self.next_seed = seed
155+
self.seed_ctr = 0
156+
157+
else:
158+
assert len(seed) == self.PART_SEED_SIZE
159+
self.next_seed = seed
160+
161+
if self.seed_done():
162+
self.seed_ctr = 0
163+
164+
def clock(self):
165+
"""Advance the state by one clock cycle. Depending on the
166+
scheduled state and seed this will alter the current state."""
167+
168+
if self.next_state == [] and self.next_seed == []:
169+
# Do nothing when neither an update nor reseed is scheduled.
170+
return
171+
172+
if self.seed_type == SeedType.KEY_IV:
173+
# Seeding takes precedence over updating.
174+
if self.next_seed != []:
175+
self.state = self.next_seed
176+
177+
elif self.next_state != []:
178+
self.state = self.next_state
179+
if not self.seed_done():
180+
self.seed_ctr += 1
181+
182+
elif self.seed_type == SeedType.STATE_FULL:
183+
# Seeding takes precedence over updating.
184+
if self.next_seed != []:
185+
self.state = self.next_seed
186+
self.seed_ctr += 1
187+
188+
elif self.next_state != []:
189+
self.state = self.next_state
190+
191+
else:
192+
# Update and seeding in the same clock interval is allowed.
193+
if self.next_state != []:
194+
self.state = self.next_state
195+
if self.next_seed != []:
196+
if self.seed_ctr == self.seed_rnd - 1:
197+
self.state[self.state_size - self.last_part_seed_size:] = (
198+
self.next_seed[: self.last_part_seed_size]
199+
)
200+
else:
201+
self.state[32 * self.seed_ctr: 32 * (self.seed_ctr + 1)] = (
202+
self.next_seed
203+
)
204+
205+
self.seed_ctr += 1
206+
207+
self.next_state = []
208+
self.next_seed = []
209+
210+
def keystream(self):
211+
"""Returns the generated keystream for the current clock
212+
interval."""
213+
return self.ks
214+
215+
def seed_done(self) -> None:
216+
"""Returns true if the seeding procedure has been completed."""
217+
return self.seed_rnd == self.seed_ctr
218+
219+
def trivium_update(self, state):
220+
mul_90_91 = state[90] & state[91]
221+
add_65_92 = state[65] ^ state[92]
222+
223+
mul_174_175 = state[174] & state[175]
224+
add_161_176 = state[161] ^ state[176]
225+
226+
mul_285_286 = state[285] & state[286]
227+
add_242_287 = state[242] ^ state[287]
228+
229+
t0 = state[68] ^ (mul_285_286 ^ add_242_287)
230+
t1 = state[170] ^ (add_65_92 ^ mul_90_91)
231+
t2 = state[263] ^ (mul_174_175 ^ add_161_176)
232+
233+
state[0:93] = [t0] + state[0:92]
234+
state[93:177] = [t1] + state[93:176]
235+
state[177:288] = [t2] + state[177:287]
236+
237+
return add_65_92 ^ add_161_176 ^ add_242_287
238+
239+
def bivium_update(self, state):
240+
mul_90_91 = state[90] & state[91]
241+
add_65_92 = state[65] ^ state[92]
242+
243+
mul_174_175 = state[174] & state[175]
244+
add_161_176 = state[161] ^ state[176]
245+
246+
t0 = state[68] ^ (mul_174_175 ^ add_161_176)
247+
t1 = state[170] ^ (add_65_92 ^ mul_90_91)
248+
249+
state[0:93] = [t0] + state[0:92]
250+
state[93:177] = [t1] + state[93:176]
251+
252+
return add_65_92 ^ add_161_176
253+
254+
255+
# Key-IV seed
256+
257+
# AVR cryptolib: Set 1, vector 0
258+
ref = i2b(
259+
int("""
260+
F980FC5474EFE87BB9626ACCCC20FF98
261+
807FCFCE928F6CE0EB21096115F5FBD2
262+
649AF249C24120550175C86414657BBB
263+
0D5420443AF18DAF9C7A0D73FF86EB38""".replace("\n", ""), 16),
264+
288,
265+
)
266+
267+
trivium = Trivium(CipherType.TRIVIUM, SeedType.KEY_IV, 64)
268+
269+
key = i2b(0x01000000000000000000, 80)
270+
iv = [0] * 80
271+
272+
trivium.seed(key + iv)
273+
trivium.clock()
274+
275+
while not trivium.seed_done():
276+
trivium.update()
277+
trivium.clock()
278+
279+
assert trivium.seed_done()
280+
281+
print("%072x" % b2i(trivium.state))
282+
283+
keystream = []
284+
for _ in range(8):
285+
trivium.update()
286+
trivium.clock()
287+
keystream.extend(trivium.keystream())
288+
289+
assert keystream == ref
290+
print("%0x" % (b2i(keystream)))
291+
292+
293+
# Full state seed
294+
295+
# Seed corresponds to the state after the init rounds from the first test.
296+
seed = i2b(
297+
0xC7D7C89BCC06725B3D94718106F2A0656422AF1FA457B81F0D2516A9D565893A64C1E50E, 288
298+
)
299+
300+
trivium = Trivium(CipherType.TRIVIUM, SeedType.STATE_FULL, 64)
301+
trivium.seed(seed)
302+
trivium.clock()
303+
304+
assert trivium.seed_done()
305+
306+
keystream = []
307+
for _ in range(8):
308+
trivium.update()
309+
trivium.clock()
310+
keystream.extend(trivium.keystream())
311+
312+
assert keystream == ref
313+
print("%0x" % (b2i(keystream)))
314+
315+
# Partial state seed
316+
317+
# Seed corresponds to the state after the init rounds from the first test.
318+
seed = [
319+
i2b(0x64C1E50E, Trivium.PART_SEED_SIZE),
320+
i2b(0xD565893A, Trivium.PART_SEED_SIZE),
321+
i2b(0x0D2516A9, Trivium.PART_SEED_SIZE),
322+
i2b(0xA457B81F, Trivium.PART_SEED_SIZE),
323+
i2b(0x6422AF1F, Trivium.PART_SEED_SIZE),
324+
i2b(0x06F2A065, Trivium.PART_SEED_SIZE),
325+
i2b(0x3D947181, Trivium.PART_SEED_SIZE),
326+
i2b(0xCC06725B, Trivium.PART_SEED_SIZE),
327+
i2b(0xC7D7C89B, Trivium.PART_SEED_SIZE),
328+
]
329+
330+
trivium = Trivium(CipherType.TRIVIUM, SeedType.STATE_PARTIAL, 64)
331+
332+
for i in range(9):
333+
trivium.seed(seed[i])
334+
trivium.clock()
335+
336+
assert trivium.seed_done()
337+
338+
keystream = []
339+
for _ in range(8):
340+
trivium.update()
341+
trivium.clock()
342+
keystream.extend(trivium.keystream())
343+
344+
assert keystream == ref
345+
print("%0x" % (b2i(keystream)))

0 commit comments

Comments
 (0)