-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adds simulation, attack, and metrics related to optical PUFs
Co-Authored-By: Adomas Baliuka <[email protected]>
- Loading branch information
1 parent
ed63388
commit c23ba88
Showing
15 changed files
with
385 additions
and
8 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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,116 @@ | ||
Linear Regression | ||
================= | ||
|
||
Linear Regression fits a linear function on given data. The resulting linear function, also called map, is guaranteed | ||
to be optimal with respect to the total squared error, i.e. the sum of the squared differences of actual value and | ||
predicted value. | ||
|
||
Linear Regression has many applications, in pypuf, it can be used to model :doc:`../simulation/optical` and | ||
:doc:`../simulation/arbiter_puf`. | ||
|
||
|
||
Arbiter PUF Reliability Side-Channel Attack [DV13]_ | ||
--------------------------------------------------- | ||
|
||
For Arbiter PUFs, the reliability for any given challenge :math:`c` has a close relationship with the difference in | ||
delay for the top and bottom line. When modeling the Arbiter PUF response as | ||
|
||
.. math:: | ||
r = \text{sgn}\left[ D_\text{noise} + \langle w, x \rangle \right], | ||
where :math:`x` is the feature vector corresponding to the challenge :math:`c` and :math:`w \in \mathbb{R}^n` are the | ||
weights describing the Arbiter PUF, and :math:`D_\text{noise}` is chosen from a Gaussian distribution with zero mean | ||
and variance :math:`\sigma_\text{noise}^2` to model the noise, then we can conclude that | ||
|
||
.. math:: | ||
\text{E}[r(x)] = \text{erf}\left( \frac{\langle w, x \rangle}{\sqrt{2}\sigma_\text{noise}} \right). | ||
Hence, the delay difference :math:`\langle w, x \rangle` can be approximated based on an approximation of | ||
:math:`\text{E[r(x)]}`, which can be easily obtained by an attacker. It gives | ||
|
||
.. math:: | ||
\langle w, x \rangle = \sqrt{2}\sigma_\text{noise} \cdot \text{erf}^{-1} \text{E}[r(x)]. | ||
This approximation works well even when :math:`\text{E}[r(x)]` is approximated based on only on few responses, e.g. 3 | ||
(see below). | ||
|
||
To demonstrate the attack, we initialize an Arbiter PUF simulation with noisiness chosen such that the reliability | ||
will be about 91% *on average*: | ||
|
||
>>> import pypuf.simulation, pypuf.io, pypuf.attack, pypuf.metrics | ||
>>> puf = pypuf.simulation.ArbiterPUF(n=64, noisiness=.25, seed=3) | ||
>>> pypuf.metrics.reliability(puf, seed=3).mean() | ||
0.908... | ||
|
||
We then create a CRP set using the *average* value of responses to 500 challenges, based on 5 measurements: | ||
|
||
>>> challenges = pypuf.io.random_inputs(n=puf.challenge_length, N=500, seed=2) | ||
>>> responses_mean = puf.r_eval(5, challenges).mean(axis=-1) | ||
>>> crps = pypuf.io.ChallengeResponseSet(challenges, responses_mean) | ||
|
||
Based on these approximated values ``responses_mean`` of the linear function :math:`\langle w, x \rangle`, we use | ||
linear regression to find a linear mapping with small error to fit the data. Note that we use the ``transform_atf`` | ||
function to compute the feature vector :math:`x` from the challenges :math:`c`, as the mapping is linear in :math:`x` | ||
(but not in :math:`c`). | ||
|
||
>>> attack = pypuf.attack.LeastSquaresRegression(crps, feature_map=lambda cs: pypuf.simulation.ArbiterPUF.transform_atf(cs, k=1)[:, 0, :]) | ||
>>> model = attack.fit() | ||
|
||
The linear map ``model`` will predict the delay difference of a given challenge. To obtain the predicted PUF response, | ||
this prediction needs to be thresholded to either -1 or 1: | ||
|
||
>>> model.postprocessing = model.postprocessing_threshold | ||
|
||
To measure the resulting model accuracy, we use :meth:`pypuf.metrics.similarity`: | ||
|
||
>>> pypuf.metrics.similarity(puf, model, seed=4) | ||
array([0.902]) | ||
|
||
|
||
Modeling Attack on Integrated Optical PUFs [RHUWDFJ13]_ | ||
------------------------------------------------------- | ||
|
||
The behavior of an integrated optical PUF token can be understood as a linear map | ||
:math:`T \in \mathbb{C}^{n \times m}` of the given challenge, where the value of :math:`T` are determined by the given | ||
PUF token, and :math:`n` is number of challenge pixels, and :math:`m` the number of response pixels. | ||
The speckle pattern of the PUF is a measurement of the intensity of its electromagnetic field at the output, hence the | ||
intensity at a given response pixel :math:`r_i` for a given challenge :math:`c` can be written as | ||
|
||
.. math:: | ||
r_i = \left| c \cdot T \right|^2. | ||
pypuf ships a basic simulator for the responses of :doc:`../simulation/optical`, on whose data a modeling attack | ||
can be demonstrated. We first initialize a simulation and collect challenge-response pairs: | ||
|
||
>>> puf = pypuf.simulation.IntegratedOpticalPUF(n=64, m=25, seed=1) | ||
>>> crps = pypuf.io.ChallengeResponseSet.from_simulation(puf, N=1000, seed=2) | ||
|
||
Then, we fit a linear map on the data contained in ``crps``. Note that the simulation returns *intensity* values rather | ||
than *field* values. We thus need to account for quadratic terms using an appropriate | ||
:meth:`feature map <pypuf.attack.LeastSquaresRegression.feature_map_optical_pufs_reloaded_improved>`. | ||
|
||
>>> attack = pypuf.attack.LeastSquaresRegression(crps, feature_map=pypuf.attack.LeastSquaresRegression.feature_map_optical_pufs_reloaded_improved) | ||
>>> model = attack.fit() | ||
|
||
The success of the attack can be visually inspected or quantified by the :doc:`/metrics/correlation` of the response | ||
pixels: | ||
|
||
>>> crps_test = pypuf.io.ChallengeResponseSet.from_simulation(puf, N=1000, seed=3) | ||
>>> pypuf.metrics.correlation(model, crps_test).mean() | ||
0.69... | ||
|
||
Note that the correlation can differ when additionally, post-processing of the responses is performed, e.g. by | ||
thresholding the values such that half the values give -1 and the other half 1: | ||
|
||
>>> import numpy as np | ||
>>> threshold = lambda r: np.sign(r - np.quantile(r.flatten(), .5)) | ||
>>> pypuf.metrics.correlation(model, crps_test, postprocessing=threshold).mean() | ||
0.41... | ||
|
||
|
||
API | ||
--- | ||
|
||
.. autoclass:: | ||
pypuf.attack.LeastSquaresRegression | ||
:members: __init__, fit, model, feature_map_optical_pufs_reloaded, feature_map_optical_pufs_reloaded_improved |
This file contains 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
This file contains 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,26 @@ | ||
Correlation | ||
----------- | ||
|
||
The correlation metric is useful to judge the prediction accuracy when responses are non-binary, e.g. when studying | ||
:doc:`/simulation/optical`. | ||
|
||
>>> import pypuf.simulation, pypuf.io, pypuf.attack, pypuf.metrics | ||
>>> puf = pypuf.simulation.IntegratedOpticalPUF(n=64, m=25, seed=1) | ||
>>> crps = pypuf.io.ChallengeResponseSet.from_simulation(puf, N=1000, seed=2) | ||
>>> crps_test = pypuf.io.ChallengeResponseSet.from_simulation(puf, N=1000, seed=3) | ||
>>> feature_map = pypuf.attack.LeastSquaresRegression.feature_map_optical_pufs_reloaded_improved | ||
>>> model = pypuf.attack.LeastSquaresRegression(crps, feature_map=feature_map).fit() | ||
>>> pypuf.metrics.correlation(model, crps_test).mean() | ||
0.69... | ||
|
||
Note that the correlation can differ when additionally, post-processing of the responses is performed, e.g. by | ||
thresholding the values such that half the values give -1 and the other half 1: | ||
|
||
>>> import numpy as np | ||
>>> threshold = lambda r: np.sign(r - np.quantile(r.flatten(), .5)) | ||
>>> pypuf.metrics.correlation(model, crps_test, postprocessing=threshold).mean() | ||
0.41... | ||
|
||
.. automodule:: pypuf.metrics | ||
:members: correlation, correlation_data | ||
:noindex: |
This file contains 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,8 @@ | ||
Integrated Optical PUFs | ||
======================= | ||
|
||
pypuf ships a very basic simulation of integrated optical PUFs [RHUWDFJ13]_. | ||
|
||
.. autoclass:: | ||
pypuf.simulation.IntegratedOpticalPUF | ||
:members: __init__, eval |
This file contains 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
This file contains 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
This file contains 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,104 @@ | ||
from typing import Callable, Optional | ||
|
||
import numpy as np | ||
|
||
from .base import OfflineAttack | ||
from ..io import ChallengeResponseSet | ||
from ..simulation.base import Simulation | ||
|
||
|
||
class LinearMapSimulation(Simulation): | ||
|
||
@staticmethod | ||
def postprocessing_id(responses: np.ndarray) -> np.ndarray: | ||
return responses | ||
|
||
@staticmethod | ||
def postprocessing_threshold(responses: np.ndarray) -> np.ndarray: | ||
return np.sign(responses) | ||
|
||
def __init__(self, linear_map: np.ndarray, challenge_length: int, | ||
feature_map: Optional[Callable[[np.ndarray], np.ndarray]] = None, | ||
postprocessing: Optional[Callable[[np.ndarray], np.ndarray]] = None) -> None: | ||
super().__init__() | ||
self.map = linear_map | ||
self._challenge_length = challenge_length | ||
self.feature_map = feature_map or (lambda x: x) | ||
self.postprocessing = postprocessing or self.postprocessing_id | ||
|
||
@property | ||
def challenge_length(self) -> int: | ||
return self._challenge_length | ||
|
||
@property | ||
def response_length(self) -> int: | ||
return self.map.shape[1] | ||
|
||
def eval(self, challenges: np.ndarray) -> np.ndarray: | ||
return self.postprocessing(self.feature_map(challenges) @ self.map) | ||
|
||
|
||
class LeastSquaresRegression(OfflineAttack): | ||
|
||
@staticmethod | ||
def feature_map_linear(challenges: np.ndarray) -> np.ndarray: | ||
return challenges | ||
|
||
@staticmethod | ||
def feature_map_optical_pufs_reloaded(challenges: np.ndarray) -> np.ndarray: | ||
""" | ||
Computes features of an optical PUF token using all ordered pairs of challenge bits [RHUWDFJ13]_. | ||
An optical system may be linear in these features. | ||
.. note:: | ||
This representation is redundant since it treats ordered paris of challenge bits are distinct. | ||
Actually, only unordered pairs of bits should be treated as distinct. For applications, use | ||
the function :meth:`feature_map_optical_pufs_reloaded_improved | ||
<pypuf.attack.LeastSquaresRegression.feature_map_optical_pufs_reloaded_improved>`, | ||
which achieves the same with half the number of features. | ||
:param challenges: array of shape :math:`(N, n)` representing challenges to the optical PUF. | ||
:return: array of shape :math:`(N, n^2)`, which, for each challenge, contains the flattened dyadic product of | ||
the challenge with itself. | ||
""" | ||
beta = np.einsum("...i,...j->...ij", challenges, challenges) | ||
return beta.reshape(beta.shape[:-2] + (-1,)) | ||
|
||
@staticmethod | ||
def feature_map_optical_pufs_reloaded_improved(challenges: np.ndarray) -> np.ndarray: | ||
r""" | ||
Computes features of an optical PUF token using all unordered pairs of challenge bits [RHUWDFJ13]_. | ||
An optical system may be linear in these features. | ||
:param challenges: 2d array of shape :math:`(N, n)` representing `N` challenges of length :math:`n`. | ||
:return: array of shape :math:`(N, \frac{n \cdot (n + 1)}{2})`. The result `return[i]` consists of all products | ||
of unordered pairs taken from `challenges[i]`, which has shape `(N,)`. | ||
>>> import numpy as np | ||
>>> import pypuf.attack | ||
>>> challenges = np.array([[2, 3, 5], [1, 0, 1]]) # non-binary numbers for illustration only. | ||
>>> pypuf.attack.LeastSquaresRegression.feature_map_optical_pufs_reloaded_improved(challenges) | ||
array([[ 4, 6, 10, 9, 15, 25], | ||
[ 1, 0, 1, 0, 0, 1]]) | ||
""" | ||
n = challenges.shape[1] | ||
idx = np.triu_indices(n) | ||
return np.einsum("...i,...j->...ij", challenges, challenges)[:, idx[0], idx[1]] | ||
|
||
def __init__(self, crps: ChallengeResponseSet, | ||
feature_map: Optional[Callable[[np.ndarray], np.ndarray]] = None) -> None: | ||
super().__init__(crps) | ||
self.crps = crps | ||
self.feature_map = feature_map or (lambda x: x) | ||
self._model = None | ||
|
||
def fit(self) -> Simulation: | ||
features = self.feature_map(self.crps.challenges) | ||
# TODO warn if more than one measurement | ||
linear_map = np.linalg.pinv(features) @ self.crps.responses[:, :, 0] | ||
self._model = LinearMapSimulation(linear_map, self.crps.challenge_length, self.feature_map) | ||
return self.model | ||
|
||
@property | ||
def model(self) -> Simulation: | ||
return self._model |
This file contains 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 |
---|---|---|
@@ -1,3 +1,3 @@ | ||
from .common import reliability_data, reliability, uniqueness_data, uniqueness, similarity_data, accuracy, similarity, \ | ||
bias, bias_data | ||
bias, bias_data, correlation, correlation_data | ||
from .fourier import total_influence, noise_sensitivity, influence |
Oops, something went wrong.