Skip to content
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

Beli PUF Simulation and Attack #175

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/appendix/bibliography.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions docs/attacks/beli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Beli PUF Attacks
================

[AMTW21]_

Logistic-Regression-Based
-------------------------

>>> import pypuf.simulation, pypuf.io
>>> puf = pypuf.simulation.TwoBitBeliPUF(n=64, k=1, seed=1)
>>> crps = pypuf.io.ChallengeResponseSet.from_simulation(puf, N=10000, seed=2)



>>> import pypuf.attack
>>> attack = pypuf.attack.TwoBitBeliLR(crps, seed=3, k=1, bs=256, lr=1, epochs=15)
>>> attack.fit() # doctest:+ELLIPSIS +NORMALIZE_WHITESPACE
Epoch 1/15
...
39/39 [==============================] ... accuracy: 0.9...
<pypuf.simulation.delay.TwoBitBeliPUF object at 0x...>
>>> model = attack.model

The model accuracy can be measured using the pypuf accuracy metric :meth:`pypuf.metrics.accuracy`.

>>> import pypuf.metrics
>>> pypuf.metrics.similarity(puf, model, seed=4)
array([0.963, 0.958])


API
---

.. autoclass:: pypuf.attack.TwoBitBeliLR
:members: __init__, fit, beli_output, model, history

.. autoclass:: pypuf.attack.OneBitBeliLR
:members: __init__, fit, beli_output, model, history

Performance
-----------

=== ==== ===== ============ =========== =======
n k CRPs success rate duration cores
=== ==== ===== ============ =========== =======
=== ==== ===== ============ =========== =======
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Some functionality is only implemented in the
Overview <simulation/overview>
simulation/arbiter_puf
simulation/delay
simulation/beli
simulation/bistable
simulation/optical
simulation/base
Expand Down Expand Up @@ -47,6 +48,7 @@ Some functionality is only implemented in the

Overview <attacks/overview>
Logistic Regression <attacks/lr>
Beli PUF <attacks/beli>
Multilayer Perceptron <attacks/mlp>
LMN (PUFMeter) <attacks/lmn>
attacks/linear_regression
Expand Down
54 changes: 54 additions & 0 deletions docs/simulation/beli.rst
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion pypuf/attack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .lr2021 import LRAttack2021
from .lr2021 import LRAttack2021, TwoBitBeliLR, OneBitBeliLR

from .mlp2021 import MLPAttack2021

Expand Down
143 changes: 137 additions & 6 deletions pypuf/attack/lr2021.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -33,7 +33,7 @@ def on_epoch_end(self, epoch: int, logs: dict = None) -> None:
self.model.stop_training = True

def __init__(self, crps: ChallengeResponseSet, seed: int, k: int, bs: int, lr: float, epochs: int,
stop_validation_accuracy: float = .95) -> None:
stop_validation_accuracy: float = .95, validation_set_size: float = .01) -> None:
"""
Initialize an improved Logistic Regression attack using the given parameters.

Expand All @@ -56,6 +56,8 @@ def __init__(self, crps: ChallengeResponseSet, seed: int, k: int, bs: int, lr: f
:param stop_validation_accuracy: Training is stopped when this validation accuracy is reached. Set to 1 to
deactivate.
:type stop_validation_accuracy: ``float``
:param validation_set_size: Proportion of CRPs to be used for validation, if <= 1, or else absolute number of
CRPs used to validation.
"""
super().__init__(crps)
self.crps = crps
Expand All @@ -66,6 +68,8 @@ def __init__(self, crps: ChallengeResponseSet, seed: int, k: int, bs: int, lr: f
self.epochs = epochs
self.stop_validation_accuracy = stop_validation_accuracy
self._history = None
self._keras_model = None
self.validation_split = validation_set_size if validation_set_size <= 1 else validation_set_size / len(crps)

@property
def history(self) -> Optional[dict]:
Expand All @@ -88,7 +92,7 @@ def loss(y_true: tf.Tensor, y_pred: tf.Tensor) -> tf.Tensor:
def accuracy(y_true: tf.Tensor, y_pred: tf.Tensor) -> tf.Tensor:
return tf.keras.metrics.binary_accuracy(.5 - .5 * y_true, .5 - .5 * y_pred)

def fit(self) -> Simulation:
def fit(self, verbose: bool = True) -> Simulation:
"""
Using tensorflow, runs the attack as configured and returns the obtained model.

Expand Down Expand Up @@ -135,7 +139,7 @@ def fit(self) -> Simulation:
activation=tf.keras.activations.tanh,
)(input_tensor)

model = tf.keras.Model(inputs=[input_tensor], outputs=[output])
self._keras_model = model = tf.keras.Model(inputs=[input_tensor], outputs=[output])
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=self.lr),
loss=self.loss,
Expand All @@ -146,8 +150,9 @@ def fit(self) -> Simulation:
features, labels,
batch_size=self.bs,
epochs=self.epochs,
validation_split=.01,
validation_split=self.validation_split,
callbacks=[self.AccuracyStop(self.stop_validation_accuracy)],
verbose=verbose,
).history

self._model = self.keras_to_pypuf(model)
Expand All @@ -170,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])),
]
3 changes: 2 additions & 1 deletion pypuf/attack/mlp2021.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def history(self) -> Optional[dict]:
"""
return self._history

def fit(self) -> Model:
def fit(self, verbose: bool = True) -> Model:
"""
Using tensorflow, runs the attack as configured and returns the obtained model.

Expand Down Expand Up @@ -184,6 +184,7 @@ def fit(self) -> Model:
callbacks=[self.EarlyStopCallback(self.early_stop, self.patience)],
shuffle=True,
validation_split=0.01,
verbose=verbose,
).history

# create pypuf model
Expand Down
12 changes: 8 additions & 4 deletions pypuf/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ def _add_result(self, params: dict, result: dict, memory: list = None) -> None:
logging.debug(f'Added result: {self.primary_results(row)}')
except NotImplementedError:
logging.debug('Added result.')
return row

def _save_log(self, force: bool = False) -> None:
if callable(self.logging_callback):
Expand All @@ -174,17 +175,17 @@ def run_single(self, params: dict) -> None:
logging.debug(f'Running {self.__class__.__name__} for {params}')

memory, result = memory_usage((self.run, [], params), retval=True)
self._add_result(params, result, memory)
return self._add_result(params, result, memory)

def run_block(self, index: int, total: int) -> None:
parameter_matrix = self._cached_parameter_matrix
n = len(parameter_matrix)
self.run_batch(parameter_matrix[int(index / total * n):int((index + 1) / total * n)])

def run_all(self) -> None:
self.run_batch(self._cached_parameter_matrix)
def run_all(self, use_tqdm: bool = False) -> None:
self.run_batch(self._cached_parameter_matrix, use_tqdm=use_tqdm)

def run_batch(self, batch: list) -> None:
def run_batch(self, batch: list, use_tqdm: bool = False) -> None:
unfinished_parameters = [
params for params in batch if self._hash_parameters(params) not in self.results.known_results()
]
Expand All @@ -198,6 +199,9 @@ def run_batch(self, batch: list) -> None:
f'study total {total} jobs)')

ran = 0
if use_tqdm:
from tqdm import tqdm
unfinished_parameters = tqdm(unfinished_parameters)
for params in unfinished_parameters:
logging.debug(f'Progress: {ran/unfinished:.1%} session, {(ran + finished)/len(batch):.1%} batch, '
f'{(ran + finished)/total:.1%} total')
Expand Down
Loading