diff --git a/docs/appendix/bibliography.rst b/docs/appendix/bibliography.rst index 8f77c2a3..648d6bd5 100644 --- a/docs/appendix/bibliography.rst +++ b/docs/appendix/bibliography.rst @@ -6,6 +6,8 @@ Bibliography .. [AM21] Aghaie, A. & Moradi, A. Inconsistency of Simulation and Practice in Delay-based Strong PUFs. IACR Transactions on Cryptographic Hardware and Embedded Systems 520–551 (2021) doi:10.46586/tches.v2021.i3.520-551. +.. [AMTW21] Aghaie, A. & Moradi, A. & Tobisch, J. & Wisiol, N. Generalization of Arbiter PUF -- New Constructions and + Novel Analyses. Unpublished (2021). .. [AZ17] Alkatheiri, M. S. & Zhuang, Y. Towards fast and accurate machine learning attacks of feed-forward arbiter PUFs. in 2017 IEEE Conference on Dependable and Secure Computing 181–187 (2017). doi:10.1109/DESEC.2017.8073845. .. [AZA18] Aseeri, A. O., Zhuang, Y. & Alkatheiri, M. S. A Machine Learning-Based Security Vulnerability Study on XOR diff --git a/docs/attacks/beli.rst b/docs/attacks/beli.rst index 4e5ffbbd..666b1d2b 100644 --- a/docs/attacks/beli.rst +++ b/docs/attacks/beli.rst @@ -17,7 +17,7 @@ Logistic-Regression-Based >>> attack.fit() # doctest:+ELLIPSIS +NORMALIZE_WHITESPACE Epoch 1/15 ... -36/36 [==============================] ... loss: 0.2... - accuracy: 0.9... +39/39 [==============================] ... accuracy: 0.9... >>> model = attack.model diff --git a/docs/index.rst b/docs/index.rst index cb65d03e..abb45de1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Some functionality is only implemented in the Overview simulation/arbiter_puf simulation/delay + simulation/beli simulation/bistable simulation/optical simulation/base @@ -47,6 +48,7 @@ Some functionality is only implemented in the Overview Logistic Regression + Beli PUF Multilayer Perceptron LMN (PUFMeter) attacks/linear_regression diff --git a/docs/simulation/beli.rst b/docs/simulation/beli.rst new file mode 100644 index 00000000..c51c1691 --- /dev/null +++ b/docs/simulation/beli.rst @@ -0,0 +1,54 @@ +Beli PUF and XOR Beli PUF +========================= + +Intro to Beli PUF, Motvation, Design, blah [AMTW21]_. + +To simulate a basic Beli PUF with two output bits, use + +>>> import pypuf.simulation, pypuf.io +>>> puf = pypuf.simulation.TwoBitBeliPUF(n=32, k=1, seed=1) +>>> crps = pypuf.io.random_inputs(n=32, N=3, seed=2) +>>> puf.eval(crps) +array([[-1, -1], + [ 1, 1], + [-1, -1]]) + +Beli PUF can also output the index of the delay line with the fastest signal for a given challenge, + +>>> puf = pypuf.simulation.BeliPUF(n=32, k=1, seed=1) +>>> puf.eval(crps) +array([[3], + [0], + [3]]) + +Observe that the two Beli PUF instances above use the same internal delays and differ only in the output format +specification. + +:class:`OneBitBeliPUF` is a Beli PUF version which returns the XOR value of the :class:`TwoBitBeliPUF` version shown +above, + +>>> puf = pypuf.simulation.OneBitBeliPUF(n=32, k=1, seed=1) +>>> puf.eval(crps) +array([1, 1, 1]) + +All Beli PUFs shown above can be arranged into an XOR Beli PUF by using the `k` parameter when instantiating: + +>>> puf = pypuf.simulation.OneBitBeliPUF(n=32, k=4, seed=1) +>>> puf.eval(crps) +array([-1, 1, -1]) + +.. note:: + pypuf currently does not implement noisy Beli PUF simulation. + However, it ships CRP data of a Beli PUF implemented in FPGA. + TODO add link. + + +API +--- + +.. autoclass:: pypuf.simulation.BeliPUF + :members: __init__, challenge_length, response_length, eval, val, signal_path, features + +.. autoclass:: pypuf.simulation.TwoBitBeliPUF + +.. autoclass:: pypuf.simulation.OneBitBeliPUF diff --git a/pypuf/attack/__init__.py b/pypuf/attack/__init__.py index d0158424..697c972c 100644 --- a/pypuf/attack/__init__.py +++ b/pypuf/attack/__init__.py @@ -1,4 +1,4 @@ -from .lr2021 import LRAttack2021 +from .lr2021 import LRAttack2021, TwoBitBeliLR, OneBitBeliLR from .mlp2021 import MLPAttack2021 diff --git a/pypuf/attack/lr2021.py b/pypuf/attack/lr2021.py index 51e3e3b4..b7475c54 100644 --- a/pypuf/attack/lr2021.py +++ b/pypuf/attack/lr2021.py @@ -1,11 +1,11 @@ -from typing import Optional +from typing import Optional, List import numpy as np import tensorflow as tf from .base import OfflineAttack from ..io import ChallengeResponseSet -from ..simulation import XORArbiterPUF +from ..simulation import XORArbiterPUF, BeliPUF, TwoBitBeliPUF, OneBitBeliPUF from ..simulation.base import Simulation, LTFArray @@ -175,3 +175,129 @@ def keras_to_pypuf(self, keras_model: tf.keras.Model) -> LTFArray: bias[l] = layer_weights[1] return LTFArray(weight_array=weights, bias=bias, transform=XORArbiterPUF.transform_atf) + + +class BeliLR(LRAttack2021): + + model_class = None + + @staticmethod + def beli_output(output_delays: List[tf.Tensor]) -> List[tf.Tensor]: + raise NotImplementedError + + def beli_model(self, input_tensor: tf.Tensor) -> List[tf.Tensor]: + internal_delays = tf.keras.layers.Dense( + units=1, + use_bias=False, + kernel_initializer=tf.keras.initializers.RandomNormal(mean=10, stddev=.05), + activation=None, + ) + output_delays = [internal_delays(input_tensor[:, i]) for i in range(input_tensor.shape[1])] + return self.beli_output(output_delays) + + def fit(self, verbose: bool = True) -> Simulation: + """ + Using tensorflow, runs the attack as configured and returns the obtained model. + + :param verbose: If true (the default), tensorflow will write progress information to stdout. + :return: Model of the Beli PUF under attack. + """ + tf.random.set_seed(self.seed) + + n = self.crps.challenge_length + k = self.k + + input_tensor = tf.keras.Input(shape=(4, 4 * n)) + beli_models = tf.transpose( + [self.beli_model(input_tensor) for _ in range(k)], # by list comprehension, k-axis is axis 0 + (2, 1, 0, 3) # swap k-axis to axis 2 and keep sample axis at axis 0 + ) # output dim: (sample, m, k, 1) + xor = tf.math.reduce_prod(beli_models, axis=2) # xor along k-axis + outputs = tf.keras.layers.Activation(tf.keras.activations.tanh)(xor) + + model = tf.keras.Model(inputs=input_tensor, outputs=outputs) + model.compile( + optimizer=tf.keras.optimizers.Adadelta(learning_rate=self.lr), + loss=self.loss, + metrics=[self.accuracy], + ) + self._keras_model = model + + self._history = model.fit( + BeliPUF.features(self.crps.challenges), + self.crps.responses, + batch_size=self.bs, + epochs=self.epochs, + validation_split=self.validation_split, + callbacks=[self.AccuracyStop(self.stop_validation_accuracy)], + verbose=verbose, + ).history + + self._model = self.keras_to_pypuf(model) + return self.model + + def keras_to_pypuf(self, keras_model: tf.keras.Model) -> LTFArray: + """ + Given a Keras model that resulted from the attack of the :meth:`fit` method, constructs an + :class:`pypuf.simulation.BeliPUF` that computes the same model. + """ + delays = np.array([ + layer.get_weights()[0].squeeze().reshape((8, -1)) + for layer in keras_model.layers + if isinstance(layer, tf.keras.layers.Dense)] + ) + + k = delays.shape[0] + n = delays.shape[2] * 2 + + pypuf_model = self.model_class(n=n, k=k, seed=0) + pypuf_model.delays = delays + + return pypuf_model + + +class TwoBitBeliLR(BeliLR): + model_class = TwoBitBeliPUF + + @staticmethod + def beli_output(output_delays: List[tf.Tensor]) -> List[tf.Tensor]: + r""" + Returns a continuous estimate of the two output bits. + + Let :math:`d_i` be the delay on delay line :math:`i`. + + For the first bit, :math:`\min\{d_2,d_3\} - \min\{d_0,d_1\}` is returned. + This expression is positive if and only if :math:`d_0` or :math:`d_1` is the fastest signal. + As first response bit is positive if delay :math:`d_0` or :math:`d_1` is fastest, + this expression is an approximation of the first response bit value. + + A similar argument holds for the second response bit. + """ + Min = tf.keras.layers.Minimum + d = output_delays + return [ + Min()((d[2], d[3])) - Min()((d[0], d[1])), + Min()((d[1], d[3])) - Min()((d[0], d[2])), + ] + + +class OneBitBeliLR(BeliLR): + model_class = OneBitBeliPUF + + @staticmethod + def beli_output(output_delays: List[tf.Tensor]) -> List[tf.Tensor]: + r""" + Returns a continuous estimate of the output bit. + + Let :math:`d_i` be the delay on delay line :math:`i`. + + :math:`\min\{d_1,d_2\} - \min\{d_0,d_3\}` is returned. + This expression is positive if and only if :math:`d_0` or :math:`d_3` is the fastest signal. + As the response bit is positive if and only if the delay :math:`d_0` or :math:`d_3` is fastest, + this expression is an approximation of the response bit value. + """ + Min = tf.keras.layers.Minimum + d = output_delays + return [ + Min()((d[1], d[2])) - Min()((d[0], d[3])), + ] diff --git a/pypuf/simulation/__init__.py b/pypuf/simulation/__init__.py index 5b6f9c7a..751d822c 100644 --- a/pypuf/simulation/__init__.py +++ b/pypuf/simulation/__init__.py @@ -3,6 +3,6 @@ from .bistable import XORBistableRingPUF, BistableRingPUF from .delay import XORArbiterPUF, XORFeedForwardArbiterPUF, ArbiterPUF, LightweightSecurePUF, RandomTransformationPUF, \ - PermutationPUF, InterposePUF, FeedForwardArbiterPUF + PermutationPUF, InterposePUF, FeedForwardArbiterPUF, BeliPUF, OneBitBeliPUF, TwoBitBeliPUF from .optical import IntegratedOpticalPUF diff --git a/pypuf/simulation/delay.py b/pypuf/simulation/delay.py index 6351e05b..e274fabb 100644 --- a/pypuf/simulation/delay.py +++ b/pypuf/simulation/delay.py @@ -650,3 +650,213 @@ def eval(self, challenges: ndarray) -> ndarray: ) assert down_challenges.shape == (N, n + 1) return self.down.eval(down_challenges) + + +class BeliPUF(Simulation): + """ + Simulation of the Beli PUF, an MPDL PUF with four delay lines. + Similar to the Arbiter PUF simulation, this simulation is based on the addition of delays based + on the path the signal is taking. In contrast to the Arbiter PUF simulation, the simulation of + the Beli PUF is not optimized with respect to the number of parameters. + + This class specifies the output of Beli PUF as the index of the delay line with the fastest signal, + given as an integer. This behavior is modified by child classes such as + :class:`TwoBitBeliPUF` and :class:`OneBitBeliPUF` to output Boolean values instead. + """ + + def __init__(self, n: int, k: int, seed: int, loc: float = 10, scale: float = .05) -> None: + """ + Initialize a Beli PUF simulation. + + :param n: Challenge length. + :type n: `int` + :param k: Number of Beli PUFs in this XOR Beli PUF. Use 1 for regular Beli PUF. + :type k: `int` + :param seed: Seed that determines the internal delays of the Beli PUF simulation. + :type seed: `int` + """ + if n % 2 != 0: + raise ValueError(f"Challenges to Beli PUF need to have even length, but {n} was given.") + self.k, self.n = k, n + m = 2 # two multiplexers per stage + + # The delays are stored in a (8, n // 2)-shaped matrix. delays[i, j] holds the delays for the j-th stage as + # follows: + # delays[0, j] is the top-top delay of the top stage + # delays[1, j] is the bottom-bottom delay of the top stage + # delays[2, j] is the bottom-top delay of the top stage + # delays[3, j] is the top-bottom delay of the top stage + # delays[4, j] is the top-top delay of the bottom stage + # delays[5, j] is the bottom-bottom delay of the bottom stage + # delays[6, j] is the bottom-top delay of the bottom stage + # delays[7, j] is the top-bottom delay of the bottom stage + self.delays = default_rng(self.seed(f"BeliPUF {seed}")).normal( + loc=loc, + scale=scale, + size=( + k, # individual delays for each of the k Beli PUFs + m * 4, # 4 delays per multiplexer + n // m, # number of stages in Beli PUF + ), + ) + + @property + def challenge_length(self) -> int: + return self.n + + def eval(self, challenges: array) -> array: + r""" + Evaluate Beli PUF on the given list of challenges and return the index of the delay line with the lowest delay. + + :param challenges: Array of challenges with bit values in :math:`\{-1,1\}` of shape (#challenges, #bits). + :return: array of shape (N, k) + """ + delays = self.val(challenges) + return np.argmin(delays, axis=2) + + def val(self, challenges: array) -> array: + r""" + Computes the delay values for each of the four signals for each of the N given challenges. + + .. note:: + This method uses numpy "fancy" indexing and summation to compute delays. + The delays can also be computed from the feature vectors using a matrix product with + ``self.features(challenges) @ self.delays.flatten()``. + + :param challenges: Array of challenges with bit values in :math:`\{-1,1\}` of shape (#challenges, #bits). + :return: Array of delays (floats) of shape (#challenges, k, 4). + """ + N = challenges.shape[0] + k = self.k + delays = np.zeros(shape=(N, k, 4)) + for l in range(self.k): + delays[:, l, :] = self.delays[l].flatten()[self.delay_indices(challenges)].sum(axis=2) + return delays + + @classmethod + def delay_indices(cls, challenges: array) -> array: + _, n = challenges.shape + path = cls.signal_path(challenges) + delay_indices_offset = np.array([8 * i for i in range(n // 2)], dtype=int) + return delay_indices_offset + path + + @staticmethod + def signal_path(challenges: array) -> array: + r""" + For each challenge given, for each of the four signals, computes the path the signal takes through the Beli PUF, + specified by the index of the relevant delay in ``self.delays.flatten()``. + + :param challenges: Array of challenges with bit values in :math:`\{-1,1\}` of shape (#challenges, #bits). + :return: Array of signal paths of dimension (#challenges, #signals = 4, #stages) with each value indicating the + relevant delay in ``self.delays.flatten()``. + """ + N, n = challenges.shape + + # Step 1: track the position of each signal after each stage in `location` + # 1: top multiplexer, top output + # -1: top multiplexer, bottom output + # 2: bottom multiplexer, top output + # -2: bottom multiplexer, bottom output + location = np.zeros(shape=(N, 4, n + 1), dtype=np.int8) # shape: (#challenges, #signals, 2 * #stages + 1) + location[:, :, n] = [1, -1, 2, -2] # signal positions we start with + location[:, :, n] = [1, 2, -1, -2] # TODO alt. signal positions we start with (matches FPGA data) + + for i in reversed(range(1, n + 1, 2)): + # change signal locations according to Beli PUF fixed signal permutation + location[location[:, :, i + 1] == 1, i] = 1 + location[location[:, :, i + 1] == -1, i] = 2 + location[location[:, :, i + 1] == 2, i] = -1 + location[location[:, :, i + 1] == -2, i] = -2 + + # change signal locations according to challenge given + # 1: top-top and bottom-bottom, -1: top-bottom and bottom-top + for j in range(4): # challenge is applied for each of the four signals individually + top_multiplexer_idx = np.abs(location[:, j, i]) == 1 # select values for top multiplexer + location[top_multiplexer_idx, j, i - 1] = ( + location[top_multiplexer_idx, j, i] * challenges[top_multiplexer_idx, i - 1] # apply challenge bit + ) + bottom_multiplexer_idx = np.abs(location[:, j, i]) == 2 # select values for bottom multiplexer + location[bottom_multiplexer_idx, j, i - 1] = ( + location[bottom_multiplexer_idx, j, i] * challenges[bottom_multiplexer_idx, i] # apply chal. bit + ) + + # compress locations to only keep locations resulting from multiplexer and fixed transform + location = location[:, :, ::2] + + # Step 2: derive signal path from sequence of signal locations + path = np.zeros(shape=(N, 4, n // 2), dtype=np.int8) - 1 + for i in range(0, n // 2): + # top multiplexer + path[(location[:, :, i] == 1) & (location[:, :, i + 1] == 1), i] = 0 # top-top + path[(location[:, :, i] == -1) & (location[:, :, i + 1] == 2), i] = 1 # bottom-bottom + path[(location[:, :, i] == -1) & (location[:, :, i + 1] == 1), i] = 2 # bottom-top + path[(location[:, :, i] == 1) & (location[:, :, i + 1] == 2), i] = 3 # top-bottom + + # bottom multiplexer + path[(location[:, :, i] == 2) & (location[:, :, i + 1] == -1), i] = 4 # top-top + path[(location[:, :, i] == -2) & (location[:, :, i + 1] == -2), i] = 5 # bottom-bottom + path[(location[:, :, i] == -2) & (location[:, :, i + 1] == -1), i] = 6 # bottom-top + path[(location[:, :, i] == 2) & (location[:, :, i + 1] == -2), i] = 7 # top-bottom + + return path + + @classmethod + def features(cls, challenges: array) -> array: + r""" + For given challenges, computes the feature vectors for each signal. + + :param challenges: Array of challenges with bit values in :math:`\{-1,1\}` of shape (#challenges, #bits). + :return: Array of feature vector over :math:`\{0,1\}` of shape (#challenges, 4, 4*#challenge bits). + """ + N, n = challenges.shape + features = np.zeros((N, 4, 4 * n), dtype=np.uint8) + np.put_along_axis(features, cls.delay_indices(challenges), 1, axis=-1) + return features + + +class TwoBitBeliPUF(BeliPUF): + """ + Version of Beli PUF with two output bits. The output bits are defined as follows: + + ============================== ================= ================= + Delay line with fastest signal Output 0 Output 1 + ============================== ================= ================= + 0 1 1 + 1 1 -1 + 2 -1 1 + 3 -1 -1 + ============================== ================= ================= + """ + + @property + def response_length(self) -> int: + return 2 + + def eval(self, challenges: array) -> array: + fastest_delay_line = super().eval(challenges) + return (1 - 2 * np.stack([fastest_delay_line // 2, fastest_delay_line % 2], axis=-1)).prod(axis=1) + + +class OneBitBeliPUF(TwoBitBeliPUF): + """ + Version of Beli PUF with only one output bit. The output bit is defined as follows: + + ============================== ================= ================= ====== + Delay line with fastest signal Internal Output 0 Internal Output 1 Output + ============================== ================= ================= ====== + 0 1 1 1 + 1 1 -1 -1 + 2 -1 1 -1 + 3 -1 -1 1 + ============================== ================= ================= ====== + + This definition corresponds to the XOR of the two output bits of the :class:`TwoBitBeliPUF`. + """ + + @property + def response_length(self) -> int: + return 1 + + def eval(self, challenges: array) -> array: + two_bit_response = super().eval(challenges) + return np.prod(two_bit_response, axis=1) diff --git a/test/attack/test_beli.py b/test/attack/test_beli.py new file mode 100644 index 00000000..37a760ec --- /dev/null +++ b/test/attack/test_beli.py @@ -0,0 +1,41 @@ +import pytest + +import pypuf.io +import pypuf.metrics +from pypuf.simulation import OneBitBeliPUF, TwoBitBeliPUF +from pypuf.attack import OneBitBeliLR, TwoBitBeliLR + + +def run_attack(puf_class, k, attack_class, + n=32, N=20000, bs=256, lr=1, seed=1, epochs=5, stop_validation_accuracy=.95, verbose=False): + puf = puf_class(n=n, k=k, seed=seed) + crps = pypuf.io.ChallengeResponseSet.from_simulation(puf, N=N + 1000, seed=seed + 1) + attack = attack_class( + crps, seed=seed + 2, + k=k, bs=bs, lr=lr, epochs=epochs, + stop_validation_accuracy=stop_validation_accuracy, + ) + attack.fit(verbose=verbose) + return pypuf.metrics.similarity(puf, attack.model, seed=seed + 3).min() + + +def test_two_bit_beli_puf_attack(): + assert run_attack(puf_class=TwoBitBeliPUF, k=1, attack_class=TwoBitBeliLR) > .9 + + +def test_one_bit_beli_puf_attack(): + assert run_attack(puf_class=OneBitBeliPUF, k=1, attack_class=OneBitBeliLR) > .9 + + +def test_xor_two_bit_beli_puf_attack(): + assert run_attack(puf_class=TwoBitBeliPUF, k=2, attack_class=TwoBitBeliLR) > .9 + + +def test_xor_one_bit_beli_puf_attack(): + assert run_attack(puf_class=OneBitBeliPUF, k=2, attack_class=OneBitBeliLR) > .9 + + +@pytest.mark.parametrize("puf_class, attack_class", [(OneBitBeliPUF, OneBitBeliLR), (TwoBitBeliPUF, TwoBitBeliLR)]) +@pytest.mark.parametrize("k, N", [(2, 100000), (3, 100000), (4, 150000)]) +def test_large_xor_attack(puf_class, attack_class, k, N): + assert run_attack(puf_class=puf_class, attack_class=attack_class, k=k, n=32, N=N) > .9 diff --git a/test/simulation/test_delay.py b/test/simulation/test_delay.py index 8e2df145..0c1e7215 100644 --- a/test/simulation/test_delay.py +++ b/test/simulation/test_delay.py @@ -169,3 +169,99 @@ def test_ff_many_loops() -> None: assert set(np.unique(puf.eval(pypuf.io.random_inputs(n=puf.challenge_length, N=100, seed=1)))) == {-1, 1} assert -.5 < pypuf.metrics.bias(puf, seed=1) < .5 assert pypuf.metrics.bias(puf, seed=1) != 0 + + +def beli_puf_debug_weights(n: int) -> np.ndarray: + return np.array([ + 10**(n // 2 - j - 1) * np.array(range(8)) + for j in range(n // 2) + ]).reshape((1, n // 2, 8)) + + +def zeros_except(n, exceptions): + a = np.zeros(n, dtype=np.int8) + a[exceptions] = 1 + return a + + +def test_beli_puf_delay() -> None: + beli_puf = pypuf.simulation.BeliPUF(n=4, k=1, seed=1) + beli_puf.delays = beli_puf_debug_weights(n=4) + challenges = np.array([ + [-1, -1, -1, -1], # all crossed + [1, 1, 1, 1], # all straight + [-1, 1, -1, 1], # top crossed, bottom straight + [1, -1, 1, -1], # top straight, bottom crossed + [1, -1, 1, 1], + ]) + + paths = beli_puf.signal_path(challenges) + # challenge with all crossed paths + assert (paths[0, 0] == [6, 2]).all() + assert (paths[0, 1] == [2, 3]).all() + assert (paths[0, 2] == [7, 6]).all() + assert (paths[0, 3] == [3, 7]).all() + + # challenge with all straight paths + assert (paths[1, 0] == [0, 0]).all() + assert (paths[1, 1] == [4, 1]).all() + assert (paths[1, 2] == [1, 4]).all() + assert (paths[1, 3] == [5, 5]).all() + + # challenge with top crossed, bottom straight + assert (paths[2, 0] == [4, 2]).all() + assert (paths[2, 1] == [2, 3]).all() + assert (paths[2, 2] == [3, 4]).all() + assert (paths[2, 3] == [5, 5]).all() + + # challenge with top straight, bottom crossed + assert (paths[3, 0] == [0, 0]).all() + assert (paths[3, 1] == [6, 1]).all() + assert (paths[3, 2] == [7, 6]).all() + assert (paths[3, 3] == [1, 7]).all() + + assert (paths[4, 0] == [0, 0]).all() + assert (paths[4, 1] == [6, 1]).all() + assert (paths[4, 2] == [1, 4]).all() + assert (paths[4, 3] == [7, 5]).all() + + delay = beli_puf.val(challenges)[:, 0, :] + assert (delay[0] == [62, 23, 76, 37]).all() + assert (delay[1] == [0, 41, 14, 55]).all() + assert (delay[2] == [42, 23, 34, 55]).all() + assert (delay[3] == [0, 61, 76, 17]).all() + assert (delay[4] == [0, 61, 14, 75]).all() + + fastest = beli_puf.eval(challenges) + assert (fastest.squeeze() == [1, 0, 1, 0, 0]).all() + + two_bit_beli_puf = pypuf.simulation.TwoBitBeliPUF(n=4, k=1, seed=1) + two_bit_beli_puf.delays = beli_puf.delays + assert (two_bit_beli_puf.eval(challenges)[:, 0] == [1, 1, 1, 1, 1]).all() + assert (two_bit_beli_puf.eval(challenges)[:, 1] == [-1, 1, -1, 1, 1]).all() + + one_bit_beli_puf = pypuf.simulation.OneBitBeliPUF(n=4, k=1, seed=1) + one_bit_beli_puf.delays = beli_puf.delays + assert (one_bit_beli_puf.eval(challenges) == [-1, 1, -1, 1, 1]).all() + + features = beli_puf.features(challenges) + assert (features[0, 0] == zeros_except(16, [6, 2 + 8])).all() + assert (features @ beli_puf.delays[0].flatten() == delay).all() + + +def test_xor_beli_puf(): + n = 32 + for k in [2, 4, 8]: + for puf_type in [ + pypuf.simulation.TwoBitBeliPUF, + pypuf.simulation.OneBitBeliPUF, + ]: + beli_pufs = [puf_type(n=n, k=1, seed=seed) for seed in range(k)] + xor_beli_puf = puf_type(n=n, k=k, seed=0) + for l in range(k): + xor_beli_puf.delays[l] = beli_pufs[l].delays[0] + challenges = pypuf.io.random_inputs(n=n, N=2, seed=1) + + responses_ind = np.array([puf.eval(challenges) for puf in beli_pufs]).prod(axis=0) + responses_xor = xor_beli_puf.eval(challenges) + assert (responses_ind == responses_xor).all()