mlsca-bench 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. mlsca_bench/__init__.py +73 -0
  2. mlsca_bench/__main__.py +9 -0
  3. mlsca_bench/benchmark/__init__.py +68 -0
  4. mlsca_bench/benchmark/classical.py +350 -0
  5. mlsca_bench/benchmark/leakage.py +319 -0
  6. mlsca_bench/benchmark/metrics.py +129 -0
  7. mlsca_bench/benchmark/plots.py +99 -0
  8. mlsca_bench/benchmark/splits.json +9 -0
  9. mlsca_bench/benchmark/splits.py +374 -0
  10. mlsca_bench/cli.py +332 -0
  11. mlsca_bench/datasets/__init__.py +70 -0
  12. mlsca_bench/datasets/adapters/__init__.py +43 -0
  13. mlsca_bench/datasets/adapters/_concat.py +229 -0
  14. mlsca_bench/datasets/adapters/_hdf5.py +56 -0
  15. mlsca_bench/datasets/adapters/aes_hd_csv.py +275 -0
  16. mlsca_bench/datasets/adapters/agilent.py +231 -0
  17. mlsca_bench/datasets/adapters/ascad.py +52 -0
  18. mlsca_bench/datasets/adapters/asciiwave.py +247 -0
  19. mlsca_bench/datasets/adapters/ascon.py +57 -0
  20. mlsca_bench/datasets/adapters/chameleon.py +380 -0
  21. mlsca_bench/datasets/adapters/hdf5.py +527 -0
  22. mlsca_bench/datasets/adapters/lecroy.py +299 -0
  23. mlsca_bench/datasets/adapters/manifest.py +250 -0
  24. mlsca_bench/datasets/adapters/matlab.py +236 -0
  25. mlsca_bench/datasets/adapters/npy.py +400 -0
  26. mlsca_bench/datasets/adapters/pickled.py +131 -0
  27. mlsca_bench/datasets/adapters/rawbinary.py +188 -0
  28. mlsca_bench/datasets/adapters/scaaml.py +391 -0
  29. mlsca_bench/datasets/adapters/trs.py +233 -0
  30. mlsca_bench/datasets/adapters/two_class.py +110 -0
  31. mlsca_bench/datasets/base.py +390 -0
  32. mlsca_bench/datasets/cache.py +90 -0
  33. mlsca_bench/datasets/download.py +1463 -0
  34. mlsca_bench/datasets/errors.py +118 -0
  35. mlsca_bench/datasets/loading.py +731 -0
  36. mlsca_bench/datasets/registry.json +4056 -0
  37. mlsca_bench/datasets/registry.py +1204 -0
  38. mlsca_bench/models/__init__.py +45 -0
  39. mlsca_bench/models/attack.py +312 -0
  40. mlsca_bench/models/cnn.py +126 -0
  41. mlsca_bench/models/registry.py +182 -0
  42. mlsca_bench/py.typed +0 -0
  43. mlsca_bench-0.1.0.dist-info/METADATA +271 -0
  44. mlsca_bench-0.1.0.dist-info/RECORD +48 -0
  45. mlsca_bench-0.1.0.dist-info/WHEEL +5 -0
  46. mlsca_bench-0.1.0.dist-info/entry_points.txt +2 -0
  47. mlsca_bench-0.1.0.dist-info/licenses/LICENSE +201 -0
  48. mlsca_bench-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,73 @@
1
+ # Copyright (c) 2026 Universität der Bundeswehr München / FI CODE - ESSEC Lab.
2
+ # Licensed under the Apache License, Version 2.0, see LICENSE for details.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Public side-channel datasets, baselines and metrics for machine-learning side-channel analysis."""
6
+
7
+ from importlib.metadata import PackageNotFoundError, version as _version
8
+
9
+ try:
10
+ __version__ = _version("mlsca-bench")
11
+ except PackageNotFoundError: # running from a source tree without installing
12
+ __version__ = "0+unknown"
13
+
14
+ from .datasets import (
15
+ ArraySideChannelDataset,
16
+ CustomDownloaderRequired,
17
+ DatasetDownloadError,
18
+ DatasetFile,
19
+ DatasetMetadata,
20
+ DatasetSpec,
21
+ DatasetUnavailable,
22
+ DatasetArray,
23
+ ManualDownloadRequired,
24
+ MissingDependencyError,
25
+ SideChannelDataset,
26
+ TraceSample,
27
+ broadcast_field,
28
+ cache_root,
29
+ cache_usage,
30
+ cached_datasets,
31
+ validate_dataset,
32
+ download_dataset,
33
+ get_dataset,
34
+ list_datasets,
35
+ load_dataset,
36
+ load_registry_file,
37
+ register_dataset,
38
+ remove_cached_dataset,
39
+ saved_datasets,
40
+ unregister_dataset,
41
+ user_registry_path,
42
+ )
43
+
44
+ __all__ = [
45
+ "__version__",
46
+ "ArraySideChannelDataset",
47
+ "CustomDownloaderRequired",
48
+ "DatasetDownloadError",
49
+ "DatasetFile",
50
+ "DatasetMetadata",
51
+ "DatasetSpec",
52
+ "DatasetUnavailable",
53
+ "DatasetArray",
54
+ "ManualDownloadRequired",
55
+ "MissingDependencyError",
56
+ "SideChannelDataset",
57
+ "TraceSample",
58
+ "broadcast_field",
59
+ "cache_root",
60
+ "cache_usage",
61
+ "cached_datasets",
62
+ "download_dataset",
63
+ "get_dataset",
64
+ "list_datasets",
65
+ "load_dataset",
66
+ "load_registry_file",
67
+ "register_dataset",
68
+ "remove_cached_dataset",
69
+ "saved_datasets",
70
+ "unregister_dataset",
71
+ "user_registry_path",
72
+ "validate_dataset",
73
+ ]
@@ -0,0 +1,9 @@
1
+ # Copyright (c) 2026 Universität der Bundeswehr München / FI CODE - ESSEC Lab.
2
+ # Licensed under the Apache License, Version 2.0, see LICENSE for details.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ sys.exit(main())
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2026 Universität der Bundeswehr München / FI CODE - ESSEC Lab.
2
+ # Licensed under the Apache License, Version 2.0, see LICENSE for details.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Benchmark layer: leakage models, reproducible splits, classical attacks and metrics.
6
+
7
+ The metrics, splits and classical attacks are pure NumPy. The deep-learning
8
+ baselines live in :mod:`mlsca_bench.models` behind the optional ``eval`` extra,
9
+ and the plots in :mod:`mlsca_bench.benchmark.plots` behind the ``plot`` extra.
10
+ """
11
+
12
+ from .classical import (
13
+ CLASSICAL_ATTACKS,
14
+ ClassicalAttackResult,
15
+ run_classical,
16
+ run_cpa,
17
+ run_dpa,
18
+ run_template_attack,
19
+ )
20
+ from .leakage import (
21
+ HAMMING_WEIGHT,
22
+ SBOX,
23
+ SBOX_INV,
24
+ LeakageModel,
25
+ aes128_round_keys,
26
+ aes_round1_output,
27
+ )
28
+ from .metrics import (
29
+ RankResult,
30
+ evaluate_key_rank,
31
+ guessing_entropy,
32
+ success_rate,
33
+ )
34
+ from .splits import (
35
+ DatasetSplits,
36
+ SplitPolicy,
37
+ Subset,
38
+ describe_splits,
39
+ make_splits,
40
+ resolve_policy,
41
+ verify_splits,
42
+ )
43
+
44
+ __all__ = [
45
+ "CLASSICAL_ATTACKS",
46
+ "ClassicalAttackResult",
47
+ "DatasetSplits",
48
+ "HAMMING_WEIGHT",
49
+ "LeakageModel",
50
+ "RankResult",
51
+ "SBOX",
52
+ "SBOX_INV",
53
+ "SplitPolicy",
54
+ "Subset",
55
+ "aes128_round_keys",
56
+ "aes_round1_output",
57
+ "describe_splits",
58
+ "evaluate_key_rank",
59
+ "guessing_entropy",
60
+ "make_splits",
61
+ "resolve_policy",
62
+ "run_classical",
63
+ "run_cpa",
64
+ "run_dpa",
65
+ "run_template_attack",
66
+ "success_rate",
67
+ "verify_splits",
68
+ ]
@@ -0,0 +1,350 @@
1
+ # Copyright (c) 2026 Universität der Bundeswehr München / FI CODE - ESSEC Lab.
2
+ # Licensed under the Apache License, Version 2.0, see LICENSE for details.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Classical (non-deep-learning) SCA baselines: CPA, DPA, Gaussian templates.
6
+
7
+ These give the "classical" half of the paper's Objective 5 and slot into the
8
+ same evaluation as the DL models — they return an attack result exposing the
9
+ Guessing-Entropy / Success-Rate curves, so the benchmark runner treats them as
10
+ just more models (``"cpa"``, ``"dpa"``, ``"template"``).
11
+
12
+ * **CPA** (Brier et al., 2004) — *non-profiled* Correlation Power Analysis:
13
+ rank key guesses by the peak Pearson correlation between a Hamming-weight
14
+ power model of the S-box output and the traces.
15
+ * **DPA** (Kocher et al., 1999) — *non-profiled* single-bit Differential Power
16
+ Analysis: rank key guesses by the peak difference-of-means between the two
17
+ groups a selection bit induces.
18
+ * **Template attack** (Chari et al., 2002) — *profiled* Gaussian templates:
19
+ fit a per-class Gaussian on points of interest from the profiling set, then
20
+ score attack traces by likelihood (reuses the log-likelihood key ranking).
21
+
22
+ Everything here is pure NumPy — no PyTorch needed.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, fields, replace
28
+
29
+ import numpy as np
30
+
31
+ from ..datasets.base import SideChannelDataset
32
+ from .leakage import LeakageModel
33
+ from .metrics import RankResult, evaluate_key_rank
34
+ from .splits import SplitPolicy, make_splits
35
+
36
+ CLASSICAL_ATTACKS = ("cpa", "dpa", "template")
37
+
38
+ _EPS = 1e-12
39
+
40
+
41
+ @dataclass
42
+ class ClassicalAttackResult:
43
+ """Outcome of a classical attack (mirrors models.AttackResult, no net)."""
44
+
45
+ ranks: RankResult
46
+ n_train: int
47
+ n_test: int
48
+
49
+ @property
50
+ def guessing_entropy(self) -> np.ndarray:
51
+ return self.ranks.guessing_entropy
52
+
53
+ @property
54
+ def success_rate(self) -> np.ndarray:
55
+ return self.ranks.success_rate
56
+
57
+ @property
58
+ def traces_to_disclosure(self) -> int | None:
59
+ return self.ranks.traces_to_disclosure()
60
+
61
+
62
+ def _materialize(dataset: SideChannelDataset) -> np.ndarray:
63
+ return np.asarray(dataset.traces[:], dtype=np.float32)
64
+
65
+
66
+ def _checkpoints(limit: int, n_points: int) -> np.ndarray:
67
+ """Trace-count checkpoints: denser early, log-spaced up to ``limit``."""
68
+
69
+ if limit <= n_points:
70
+ return np.arange(1, limit + 1)
71
+ pts = np.unique(np.geomspace(1, limit, n_points).round().astype(int))
72
+ if pts[-1] != limit:
73
+ pts = np.append(pts, limit)
74
+ return pts
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Non-profiled distinguishers (CPA / DPA)
79
+ # ---------------------------------------------------------------------------
80
+ def _nonprofiled_curves(
81
+ traces: np.ndarray,
82
+ hypotheses: np.ndarray, # (N, 256): HW values for CPA, S-box out for DPA
83
+ correct_key: int,
84
+ *,
85
+ kind: str, # "cpa" | "dpa"
86
+ dpa_bit: int = 7,
87
+ n_experiments: int = 100,
88
+ order: int = 1,
89
+ max_traces: int | None = None,
90
+ n_points: int = 40,
91
+ seed: int = 0,
92
+ ) -> RankResult:
93
+ """GE / SR curves for a non-profiled distinguisher.
94
+
95
+ For each of ``n_experiments`` random trace orderings, the distinguisher is
96
+ accumulated block-by-block up to each checkpoint (so the total matmul cost
97
+ per experiment is one pass over the traces), the correct key is ranked, and
98
+ the ranks are averaged.
99
+ """
100
+
101
+ x = np.asarray(traces, dtype=np.float32)
102
+ n_traces, n_samples = x.shape
103
+ n_keys = hypotheses.shape[1]
104
+ limit = n_traces if max_traces is None else min(max_traces, n_traces)
105
+ checkpoints = _checkpoints(limit, n_points)
106
+ rng = np.random.default_rng(seed)
107
+ key = int(correct_key)
108
+
109
+ if kind == "cpa":
110
+ model = np.asarray(hypotheses, dtype=np.float32) # HW values
111
+ elif kind == "dpa":
112
+ model = ((np.asarray(hypotheses).astype(np.int64) >> dpa_bit) & 1).astype(np.float32)
113
+ else: # pragma: no cover - guarded by caller
114
+ raise ValueError(f"unknown non-profiled kind {kind!r}")
115
+
116
+ ge = np.zeros(len(checkpoints))
117
+ sr = np.zeros(len(checkpoints))
118
+ for _ in range(n_experiments):
119
+ idx = rng.permutation(n_traces)[:limit]
120
+ xo = x[idx]
121
+ mo = model[idx]
122
+
123
+ sx = np.zeros(n_samples, dtype=np.float64)
124
+ if kind == "cpa":
125
+ sxx = np.zeros(n_samples)
126
+ sh = np.zeros(n_keys)
127
+ shh = np.zeros(n_keys)
128
+ sxh = np.zeros((n_keys, n_samples))
129
+ else:
130
+ s1 = np.zeros((n_keys, n_samples))
131
+ c1 = np.zeros(n_keys)
132
+
133
+ prev = 0
134
+ for ci, cp in enumerate(checkpoints):
135
+ xb = xo[prev:cp].astype(np.float64)
136
+ mb = mo[prev:cp].astype(np.float64)
137
+ sx += xb.sum(axis=0)
138
+ n = cp
139
+ if kind == "cpa":
140
+ sxx += (xb * xb).sum(axis=0)
141
+ sh += mb.sum(axis=0)
142
+ shh += (mb * mb).sum(axis=0)
143
+ sxh += mb.T @ xb
144
+ num = n * sxh - sh[:, None] * sx[None, :]
145
+ den = np.sqrt(
146
+ np.maximum(n * shh - sh * sh, _EPS)[:, None]
147
+ * np.maximum(n * sxx - sx * sx, _EPS)[None, :]
148
+ )
149
+ score = np.abs(num / den).max(axis=1)
150
+ else: # dpa
151
+ s1 += mb.T @ xb
152
+ c1 += mb.sum(axis=0)
153
+ c0 = n - c1
154
+ with np.errstate(invalid="ignore", divide="ignore"):
155
+ mean1 = s1 / np.where(c1[:, None] > 0, c1[:, None], np.nan)
156
+ mean0 = (sx[None, :] - s1) / np.where(c0[:, None] > 0, c0[:, None], np.nan)
157
+ dom = np.abs(np.nan_to_num(mean1 - mean0, nan=0.0))
158
+ score = dom.max(axis=1)
159
+ prev = cp
160
+
161
+ score = score + rng.standard_normal(n_keys) * 1e-12 # break ties
162
+ rank = int((score > score[key]).sum())
163
+ ge[ci] += rank
164
+ sr[ci] += rank < order
165
+
166
+ ge /= n_experiments
167
+ sr /= n_experiments
168
+ return RankResult(np.asarray(checkpoints), ge, sr)
169
+
170
+
171
+ def _relabel(leakage, label: str):
172
+ """The user's leakage model with only its label type changed (any cipher)."""
173
+
174
+ if not any(f.name == "leakage" for f in fields(leakage)):
175
+ raise TypeError(f"{type(leakage).__name__} has no label type and cannot be used here.")
176
+ return replace(leakage, leakage=label)
177
+
178
+
179
+ def run_cpa(
180
+ name: str, *, leakage: LeakageModel | None = None, path: str | None = None,
181
+ policy: SplitPolicy | None = None, n_experiments: int = 100, order: int = 1,
182
+ seed: int = 0, n_points: int = 40, max_traces: int | None = None,
183
+ **load_kwargs,
184
+ ) -> ClassicalAttackResult:
185
+ """Correlation Power Analysis (non-profiled, Hamming-weight power model)."""
186
+
187
+ leakage = leakage or LeakageModel()
188
+ hw = _relabel(leakage, "hw")
189
+ with make_splits(name, policy=policy, path=path, **load_kwargs) as splits:
190
+ x = _materialize(splits.test)
191
+ hyp = hw.dataset_hypotheses(splits.test)
192
+ key = hw.true_key_byte(splits.test)
193
+ n_train = len(splits.train)
194
+ ranks = _nonprofiled_curves(
195
+ x, hyp, key, kind="cpa", n_experiments=n_experiments, order=order,
196
+ max_traces=max_traces, n_points=n_points, seed=seed,
197
+ )
198
+ return ClassicalAttackResult(ranks, n_train, len(x))
199
+
200
+
201
+ def run_dpa(
202
+ name: str, *, leakage: LeakageModel | None = None, path: str | None = None,
203
+ policy: SplitPolicy | None = None, dpa_bit: int | None = None, n_experiments: int = 100,
204
+ order: int = 1, seed: int = 0, n_points: int = 40, max_traces: int | None = None,
205
+ **load_kwargs,
206
+ ) -> ClassicalAttackResult:
207
+ """Single-bit Differential Power Analysis (non-profiled, difference of means).
208
+
209
+ ``dpa_bit`` selects the bit of the attacked intermediate; by default its most
210
+ significant bit (bit 7 of the AES S-box output).
211
+ """
212
+
213
+ leakage = leakage or LeakageModel()
214
+ idm = _relabel(leakage, "id")
215
+ width = int(idm.n_classes).bit_length() - 1 # bits of the intermediate
216
+ if dpa_bit is None:
217
+ dpa_bit = width - 1
218
+ if not 0 <= dpa_bit < width:
219
+ raise ValueError(f"dpa_bit must be between 0 and {width - 1} for {type(leakage).__name__}.")
220
+ with make_splits(name, policy=policy, path=path, **load_kwargs) as splits:
221
+ x = _materialize(splits.test)
222
+ hyp = idm.dataset_hypotheses(splits.test)
223
+ key = idm.true_key_byte(splits.test)
224
+ n_train = len(splits.train)
225
+ ranks = _nonprofiled_curves(
226
+ x, hyp, key, kind="dpa", dpa_bit=dpa_bit, n_experiments=n_experiments,
227
+ order=order, max_traces=max_traces, n_points=n_points, seed=seed,
228
+ )
229
+ return ClassicalAttackResult(ranks, n_train, len(x))
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Profiled Gaussian template attack
234
+ # ---------------------------------------------------------------------------
235
+ def _select_poi(traces: np.ndarray, labels: np.ndarray, n_poi: int, n_classes: int) -> np.ndarray:
236
+ """Top-``n_poi`` samples by SNR = var(class means) / mean(class variances)."""
237
+
238
+ n_samples = traces.shape[1]
239
+ class_means = np.zeros((n_classes, n_samples))
240
+ class_vars = np.zeros((n_classes, n_samples))
241
+ present = np.zeros(n_classes, dtype=bool)
242
+ for c in range(n_classes):
243
+ rows = traces[labels == c]
244
+ if len(rows) == 0:
245
+ continue
246
+ present[c] = True
247
+ class_means[c] = rows.mean(axis=0)
248
+ class_vars[c] = rows.var(axis=0)
249
+ signal = class_means[present].var(axis=0)
250
+ noise = class_vars[present].mean(axis=0) + _EPS
251
+ snr = signal / noise
252
+ return np.argsort(snr)[::-1][:n_poi]
253
+
254
+
255
+ def _fit_templates(traces: np.ndarray, labels: np.ndarray, n_classes: int):
256
+ """Per-class means + a shared (pooled) covariance on the POI subspace."""
257
+
258
+ n_poi = traces.shape[1]
259
+ means = np.full((n_classes, n_poi), np.nan)
260
+ pooled = np.zeros((n_poi, n_poi))
261
+ total = 0
262
+ for c in range(n_classes):
263
+ rows = traces[labels == c]
264
+ if len(rows) == 0:
265
+ continue
266
+ means[c] = rows.mean(axis=0)
267
+ centered = rows - means[c]
268
+ pooled += centered.T @ centered
269
+ total += len(rows)
270
+ pooled /= max(total - n_classes, 1)
271
+ pooled += np.eye(n_poi) * (np.trace(pooled) / n_poi * 1e-3 + _EPS) # regularize
272
+ inv_cov = np.linalg.inv(pooled)
273
+ return means, inv_cov
274
+
275
+
276
+ def _template_logliks(traces: np.ndarray, means: np.ndarray, inv_cov: np.ndarray) -> np.ndarray:
277
+ """Per-trace, per-class Gaussian log-likelihood (pooled cov ⇒ const drops)."""
278
+
279
+ n, _ = traces.shape
280
+ n_classes = means.shape[0]
281
+ ll = np.full((n, n_classes), -np.inf)
282
+ for c in range(n_classes):
283
+ if np.isnan(means[c]).any():
284
+ continue
285
+ d = traces - means[c]
286
+ ll[:, c] = -0.5 * np.einsum("ij,jk,ik->i", d, inv_cov, d)
287
+ return ll
288
+
289
+
290
+ def run_template_attack(
291
+ name: str, *, leakage: LeakageModel | None = None, path: str | None = None,
292
+ policy: SplitPolicy | None = None, n_poi: int = 20, n_experiments: int = 100,
293
+ order: int = 1, seed: int = 0, max_traces: int | None = None,
294
+ **load_kwargs,
295
+ ) -> ClassicalAttackResult:
296
+ """Gaussian template attack (profiled): fit on the profiling set, attack the
297
+ test set via likelihood, and rank keys with the shared log-likelihood metric.
298
+ The ``n_poi`` points of interest are the samples with the highest SNR on the
299
+ profiling traces."""
300
+
301
+ leakage = leakage or LeakageModel(leakage="hw")
302
+ with make_splits(name, policy=policy, path=path, **load_kwargs) as splits:
303
+ x_train = _materialize(splits.train)
304
+ y_train = leakage.dataset_labels(splits.train)
305
+ x_test = _materialize(splits.test)
306
+ hyp = leakage.dataset_hypotheses(splits.test)
307
+ key = leakage.true_key_byte(splits.test)
308
+
309
+ n_poi = min(n_poi, x_train.shape[1])
310
+ poi = _select_poi(x_train, y_train, n_poi, leakage.n_classes)
311
+ means, inv_cov = _fit_templates(x_train[:, poi], y_train, leakage.n_classes)
312
+ ll = _template_logliks(x_test[:, poi], means, inv_cov)
313
+
314
+ # log-likelihoods -> probabilities (stable softmax over classes)
315
+ ll = ll - ll.max(axis=1, keepdims=True)
316
+ probs = np.exp(ll)
317
+ probs /= probs.sum(axis=1, keepdims=True) + _EPS
318
+
319
+ ranks = evaluate_key_rank(
320
+ probs, hyp, key, n_experiments=n_experiments, order=order,
321
+ max_traces=max_traces, seed=seed,
322
+ )
323
+ return ClassicalAttackResult(ranks, len(x_train), len(x_test))
324
+
325
+
326
+ def run_classical(
327
+ name: str, dataset: str, *, leakage: LeakageModel | None = None,
328
+ path: str | None = None, seed: int = 0, n_experiments: int = 100,
329
+ **kwargs,
330
+ ) -> ClassicalAttackResult:
331
+ """Dispatch to a classical attack by name (``"cpa"``/``"dpa"``/``"template"``)."""
332
+
333
+ common = dict(leakage=leakage, path=path, seed=seed, n_experiments=n_experiments)
334
+ if name == "cpa":
335
+ return run_cpa(dataset, **common, **kwargs)
336
+ if name == "dpa":
337
+ return run_dpa(dataset, **common, **kwargs)
338
+ if name == "template":
339
+ return run_template_attack(dataset, **common, **kwargs)
340
+ raise ValueError(f"unknown classical attack {name!r}; choose from {CLASSICAL_ATTACKS}")
341
+
342
+
343
+ __all__ = [
344
+ "CLASSICAL_ATTACKS",
345
+ "ClassicalAttackResult",
346
+ "run_classical",
347
+ "run_cpa",
348
+ "run_dpa",
349
+ "run_template_attack",
350
+ ]