Skip to content

Commit 96aefcd

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 96aefcd

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed

hw/ip/prim/util/trivium.py

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

0 commit comments

Comments
 (0)