-
Notifications
You must be signed in to change notification settings - Fork 977
[otbnsim] Cycle-accurate Python model of Trivium/Bivium #29622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
andrea-caforio
wants to merge
1
commit into
lowRISC:master
Choose a base branch
from
andrea-caforio:trivium-python-model
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+385
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,385 @@ | ||
| # Copyright lowRISC contributors (OpenTitan project). | ||
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import math | ||
| from enum import IntEnum | ||
|
|
||
|
|
||
| class SeedType(IntEnum): | ||
| # This is the regular (standardized for Trivium) method for seeding the | ||
| # cipher whereby an 80-bit key and 80-bit IV are injected into the state | ||
| # before the initialization rounds are executed. | ||
| KEY_IV = 0 | ||
| # The entire state is seeded once with a seed of the same length. No | ||
| # initialization rounds are executed. | ||
| STATE_FULL = 1 | ||
| # Every seed operation fills a chunk of predefined size of the state | ||
| # starting with the least significant region until every bit of the state | ||
| # has been seeded. The seed operations can be interspersed with update | ||
| # invocations such that keystream and seeding can take place concurrently. | ||
| # Seeding and updating at the same time should only be done if the output | ||
| # width is larger than the smallest state register (84 bits for Trivium and | ||
| # Bivium) since only then we can be sure that a partial seed has diffused | ||
| # sufficiently into the state between two seed calls. See `prim_trivium.sv` | ||
| # for more details. | ||
| STATE_PARTIAL = 2 | ||
|
|
||
|
|
||
| class CipherType(IntEnum): | ||
| # Both Trivium and its simpler variant Bivium can be instantiated. Note | ||
| # only Trivium is a standardized cipher while Bivium serves a vehicule to | ||
| # study the cryptanalytic properties of this family of ciphers. Both | ||
| # primitives can be used to instantiate a PRNG. | ||
| TRIVIUM = 0 | ||
| BIVIUM = 1 | ||
|
|
||
|
|
||
| def i2b(i: int, n: int) -> list[int]: | ||
| """Convert a little endian integer `i` to a `n`-bit array with the LSB at | ||
| idx 0. The resulting bit array is padded with 0s if log2(i) < `n` until its | ||
| size is `n` bits. Regardless of `n` there will always be at least | ||
| ceil(log2(i)) bits""" | ||
| return [int(b) for b in bin(i)[2:].zfill(n)][::-1] | ||
andrea-caforio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def b2i(b: list[int]) -> int: | ||
| """Convert a bit array `b` with the LSB at idx 0 to a little endian | ||
| integer.""" | ||
| return int("".join(str(d) for d in b[::-1]), 2) | ||
|
|
||
|
|
||
| class Trivium: | ||
| """This is a cycle-accurate model of the OpenTitan Trivium primitive. | ||
|
|
||
| Instantiating this class corresponds to the cipher state after the reset. | ||
| Subsequently, two operations can be scheduled in a cycle interval, i.e., | ||
| between calls to `step`. | ||
|
|
||
| - `seed`: Pass a seed to the cipher that will appear in the state after | ||
| the next `step` call. Depending on the seed type different update | ||
| sequences are necessary to complete the initialization routines. | ||
andrea-caforio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - KEY_IV: The entire state needs to be updated 4 times over which | ||
| means ceil(4 * `STATE_SIZE` / `OUTPUT_WIDTH`) calls to `update` | ||
| and `step`. | ||
| - STATE_FULL: Having called `step` after `seed` immediately readies | ||
| the cipher for the generation of keystream bits. | ||
| - STATE_PARTIAL: ceil(`STATE_SIZE` / `PART_SEED_SIZE`) step | ||
| intervals with `seed` are required before the cipher is ready. | ||
| There can intervals without `seed` calls. This models stall in | ||
| the generation of seed bits from entropy complex. | ||
|
|
||
| - `update`: Run the state update function and generate an updated state | ||
| that replaces the current state at the end of the cycle interval, | ||
| i.e., after the `step` call. | ||
| """ | ||
|
|
||
| TRIVIUM_STATE_SIZE = 288 | ||
| BIVIUM_STATE_SIZE = 177 | ||
|
|
||
| PART_SEED_SIZE = 32 | ||
| TRIVIUM_LAST_PART_SEED_SIZE = 32 | ||
| BIVIUM_LAST_PART_SEED_SIZE = 17 | ||
|
|
||
| # Initial state after reset (see `prim_trivium_pkg.sv`). | ||
| TRIVIUM_INIT_SEED = i2b( | ||
| 0x758A442031E1C4616EA343EC153282A30C132B5723C5A4CF4743B3C7C32D580F74F1713A, | ||
| TRIVIUM_STATE_SIZE, | ||
| ) | ||
| BIVIUM_INIT_SEED = TRIVIUM_INIT_SEED[0:BIVIUM_STATE_SIZE] | ||
|
|
||
| def __init__( | ||
| self, | ||
| cipher_type: CipherType, | ||
| seed_type: SeedType, | ||
| output_width: int, | ||
| init_seed: list[int] = [], | ||
| ): | ||
| """The cipher is defined by its cipher type (see `CipherType`), its | ||
| seed type (see `SeedType`), the output (keystream) width and an | ||
| optional initialization seed.""" | ||
|
|
||
| if cipher_type == CipherType.TRIVIUM: | ||
| self.state_size = self.TRIVIUM_STATE_SIZE | ||
| self.update_func = self._trivium_update | ||
| self.last_part_seed_size = self.TRIVIUM_LAST_PART_SEED_SIZE | ||
| self.state = self.TRIVIUM_INIT_SEED[:] | ||
|
|
||
| elif cipher_type == CipherType.BIVIUM: | ||
| self.state_size = self.BIVIUM_STATE_SIZE | ||
| self.update_func = self._bivium_update | ||
| self.last_part_seed_size = self.BIVIUM_LAST_PART_SEED_SIZE | ||
| self.state = self.BIVIUM_INIT_SEED[:] | ||
|
|
||
| else: | ||
| raise ValueError("unknown cipher type:", cipher_type) | ||
|
|
||
| if init_seed != []: | ||
| if len(init_seed) != self.state_size: | ||
| raise ValueError( | ||
| f"{cipher_type.name} init seed must be the same size as the state" | ||
| ) | ||
| self.state = init_seed | ||
|
|
||
| # Depending on cipher and seed type, a different number of seed rounds | ||
| # have to be run. | ||
| if seed_type == SeedType.KEY_IV: | ||
| self.seed_rounds = math.ceil(4 * self.state_size / output_width) | ||
| self.seed_counter = 0 | ||
| elif seed_type == SeedType.STATE_FULL: | ||
| self.seed_rounds = 1 | ||
| self.seed_counter = 0 | ||
| elif seed_type == SeedType.STATE_PARTIAL: | ||
| self.seed_rounds = math.ceil(self.state_size / self.PART_SEED_SIZE) | ||
| self.seed_counter = 0 | ||
| else: | ||
| raise ValueError("unknown seed type:", seed_type) | ||
|
|
||
| self.cipher_type = cipher_type | ||
| self.seed_type = seed_type | ||
| self.output_width = output_width | ||
|
|
||
| # Scheduled state and seed for the current cycle interval. | ||
| self.next_state = [] | ||
| self.next_seed = [] | ||
|
|
||
| self.ks = [0] * output_width | ||
|
|
||
| def update(self) -> None: | ||
| """Run the state update function `OUTPUT_WIDTH`-many times and schedule | ||
| the new state to replace the current state at end of the cycle interval. | ||
| This will also generate the keystream that can be read via | ||
| `keystream`.""" | ||
|
|
||
| if self.next_state != []: | ||
| raise Exception("cannot update more than once per cycle interval") | ||
|
|
||
| self.next_state = self.state[:] | ||
| for i in range(self.output_width): | ||
| self.ks[i] = self.update_func(self.next_state) | ||
|
|
||
| def seed(self, seed) -> None: | ||
| """Schedule a new seed that depending on the seed type will be injected | ||
| into the state at the end of the cycle interval.""" | ||
|
|
||
| if self.next_seed != []: | ||
| raise Exception("cannot seed more than once per cycle interval") | ||
|
|
||
| if self.seed_type == SeedType.KEY_IV: | ||
andrea-caforio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| assert len(seed) == 160 | ||
|
|
||
| key = seed[0:80] | ||
| iv = seed[80:160] | ||
|
|
||
| if self.cipher_type == CipherType.TRIVIUM: | ||
| self.next_seed = ( | ||
| (key + [0] * 13) + (iv + [0] * 4) + ([0] * 108 + [1, 1, 1]) | ||
| ) | ||
| else: | ||
| self.next_seed = (key + [0] * 13) + (iv + [0] * 4) | ||
|
|
||
| if self.seed_done(): | ||
| self.seed_counter = 0 | ||
|
|
||
| elif self.seed_type == SeedType.STATE_FULL: | ||
| assert len(seed) == self.state_size | ||
| self.next_seed = seed | ||
| self.seed_counter = 0 | ||
|
|
||
| else: | ||
| assert len(seed) == self.PART_SEED_SIZE | ||
| self.next_seed = seed | ||
|
|
||
| if self.seed_done(): | ||
| self.seed_counter = 0 | ||
|
|
||
| def step(self): | ||
| """Advance the state by one clock cycle. Depending on the | ||
| scheduled state and seed this will alter the current state.""" | ||
|
|
||
| if self.next_state == [] and self.next_seed == []: | ||
| # Do nothing when neither an update nor reseed is scheduled. | ||
| return | ||
|
|
||
| if self.seed_type == SeedType.KEY_IV: | ||
| # Seeding takes precedence over updating. | ||
| if self.next_seed != []: | ||
| self.state = self.next_seed | ||
|
|
||
| elif self.next_state != []: | ||
| self.state = self.next_state | ||
| if not self.seed_done(): | ||
| self.seed_counter += 1 | ||
|
|
||
| elif self.seed_type == SeedType.STATE_FULL: | ||
| # Seeding takes precedence over updating. | ||
| if self.next_seed != []: | ||
| self.state = self.next_seed | ||
| self.seed_counter += 1 | ||
|
|
||
| elif self.next_state != []: | ||
| self.state = self.next_state | ||
|
|
||
| else: | ||
| # Update and seeding in the same cycle interval is allowed. In this | ||
| # case the state is first updated, then partially overwritten with | ||
| # the seed bits. | ||
| if self.next_state != []: | ||
| self.state = self.next_state | ||
| if self.next_seed != []: | ||
| if self.seed_counter == self.seed_rounds - 1: | ||
| self.state[self.state_size - self.last_part_seed_size:] = ( | ||
| self.next_seed[: self.last_part_seed_size] | ||
| ) | ||
| else: | ||
| self.state[ | ||
| self.PART_SEED_SIZE * self.seed_counter: | ||
| self.PART_SEED_SIZE * (self.seed_counter + 1) | ||
| ] = self.next_seed | ||
|
|
||
| self.seed_counter += 1 | ||
|
|
||
| self.next_state = [] | ||
| self.next_seed = [] | ||
|
|
||
| def keystream(self): | ||
| """Returns the generated keystream for the current cycle interval. Note | ||
| that an `update` call is necessary to populate the keystream.""" | ||
| return self.ks | ||
|
|
||
| def seed_done(self) -> None: | ||
| """Returns true if the seeding procedure has been completed.""" | ||
| return self.seed_rounds == self.seed_counter | ||
|
|
||
| def _trivium_update(self, state): | ||
| mul_90_91 = state[90] & state[91] | ||
| add_65_92 = state[65] ^ state[92] | ||
|
|
||
| mul_174_175 = state[174] & state[175] | ||
| add_161_176 = state[161] ^ state[176] | ||
|
|
||
| mul_285_286 = state[285] & state[286] | ||
| add_242_287 = state[242] ^ state[287] | ||
|
|
||
| t0 = state[68] ^ (mul_285_286 ^ add_242_287) | ||
| t1 = state[170] ^ (add_65_92 ^ mul_90_91) | ||
| t2 = state[263] ^ (mul_174_175 ^ add_161_176) | ||
|
|
||
| state[0:93] = [t0] + state[0:92] | ||
| state[93:177] = [t1] + state[93:176] | ||
| state[177:288] = [t2] + state[177:287] | ||
andrea-caforio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return add_65_92 ^ add_161_176 ^ add_242_287 | ||
|
|
||
| def _bivium_update(self, state): | ||
| mul_90_91 = state[90] & state[91] | ||
| add_65_92 = state[65] ^ state[92] | ||
|
|
||
| mul_174_175 = state[174] & state[175] | ||
| add_161_176 = state[161] ^ state[176] | ||
|
|
||
| t0 = state[68] ^ (mul_174_175 ^ add_161_176) | ||
| t1 = state[170] ^ (add_65_92 ^ mul_90_91) | ||
|
|
||
| state[0:93] = [t0] + state[0:92] | ||
andrea-caforio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| state[93:177] = [t1] + state[93:176] | ||
andrea-caforio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return add_65_92 ^ add_161_176 | ||
|
|
||
|
|
||
| _OUTPUT_WIDTH = 64 | ||
|
|
||
|
|
||
| def check_keystream(trivium: Trivium, ref: list[int]): | ||
| """Draw keystream bits and compare them against a test vector.""" | ||
| assert trivium.seed_done() | ||
|
|
||
| # Draw (512 / 8)-many `_OUTPUT_WIDTH`-size words from the cipher. | ||
| keystream = [] | ||
| for _ in range(_OUTPUT_WIDTH >> 3): | ||
| trivium.update() | ||
| trivium.step() | ||
| keystream.extend(trivium.keystream()) | ||
|
|
||
| assert keystream == ref | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| # Key-IV seed: | ||
| # | ||
| # Use the first key-iv test vector of the AVR cryptolib Trivium | ||
| # implementation and generate 512 bits of keystream. | ||
|
|
||
| # AVR cryptolib: Set 1, vector 0 | ||
| ref = i2b( | ||
| int( | ||
| """ | ||
| F980FC5474EFE87BB9626ACCCC20FF98 | ||
| 807FCFCE928F6CE0EB21096115F5FBD2 | ||
| 649AF249C24120550175C86414657BBB | ||
| 0D5420443AF18DAF9C7A0D73FF86EB38""".replace("\n", ""), | ||
| 16, | ||
| ), | ||
| 288, | ||
| ) | ||
|
|
||
| trivium = Trivium(CipherType.TRIVIUM, SeedType.KEY_IV, _OUTPUT_WIDTH) | ||
|
|
||
| key = i2b(0x01000000000000000000, 80) | ||
| iv = [0] * 80 | ||
|
|
||
| # Reseed twice. | ||
| for _ in range(2): | ||
| trivium.seed(key + iv) | ||
| trivium.step() | ||
|
|
||
| while not trivium.seed_done(): | ||
| trivium.update() | ||
| trivium.step() | ||
|
|
||
| check_keystream(trivium, ref) | ||
|
|
||
| # Full state seed: | ||
| # | ||
| # Use the state after the init rounds of the first test as a full state seed. | ||
|
|
||
| seed = i2b( | ||
| 0xC7D7C89BCC06725B3D94718106F2A0656422AF1FA457B81F0D2516A9D565893A64C1E50E, 288 | ||
| ) | ||
|
|
||
| trivium = Trivium(CipherType.TRIVIUM, SeedType.STATE_FULL, _OUTPUT_WIDTH) | ||
|
|
||
| # Reseed twice. | ||
| for _ in range(2): | ||
| trivium.seed(seed) | ||
| trivium.step() | ||
|
|
||
| check_keystream(trivium, ref) | ||
|
|
||
| # Partial state seed | ||
| # | ||
| # Use the state after the init rounds of the first test as a partial state seed. | ||
|
|
||
| seed = [ | ||
| i2b(0x64C1E50E, Trivium.PART_SEED_SIZE), | ||
| i2b(0xD565893A, Trivium.PART_SEED_SIZE), | ||
| i2b(0x0D2516A9, Trivium.PART_SEED_SIZE), | ||
| i2b(0xA457B81F, Trivium.PART_SEED_SIZE), | ||
| i2b(0x6422AF1F, Trivium.PART_SEED_SIZE), | ||
| i2b(0x06F2A065, Trivium.PART_SEED_SIZE), | ||
| i2b(0x3D947181, Trivium.PART_SEED_SIZE), | ||
| i2b(0xCC06725B, Trivium.PART_SEED_SIZE), | ||
| i2b(0xC7D7C89B, Trivium.PART_SEED_SIZE), | ||
| ] | ||
|
|
||
| trivium = Trivium(CipherType.TRIVIUM, SeedType.STATE_PARTIAL, _OUTPUT_WIDTH) | ||
|
|
||
| # Reseed twice. | ||
| for _ in range(2): | ||
| # Need ceil(288/32) seed calls to fill every bit of the state. | ||
| for i in range(9): | ||
| trivium.seed(seed[i]) | ||
| trivium.step() | ||
|
|
||
| check_keystream(trivium, ref) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question about Trivium: Does this mean that we can generate usable (for e.g. masking) randomness while re-seeding the trivium PRNG? So we do not have to wait the initial 64 cycles before we get randomness?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah thanks to your nice description below I think this means that we can continue using the old state to generate a keystream but at the same time feed in a new seed. And once we have fully reseeded it, the keystream produces value based upon the new seed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, Bivium will be seeded in 6 32-bit` chunks that make it possible to seed while updating at the same time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please note that this works if the output width is greater than the width of the smallest shift register internally. For Bivium, this is 84 bits.
What also helps is that there is always a little delay (IIRC 6 clock cycles) between two subsequent 32-bit words for reseeding as there is CDC crossing on the EDN interface. So in practice we will do 6 updates between to seed words. The first seed word will have propagated into most of the state after this already.
In the header of
hw/ip/prim/rtl/prim_trivium.svI gave more insight on this matter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanations. Maybe worth to point to this additional info?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expanded the comment.