oscura 0.3.0__py3-none-any.whl → 0.5.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.
- oscura/__init__.py +1 -7
- oscura/acquisition/__init__.py +147 -0
- oscura/acquisition/file.py +255 -0
- oscura/acquisition/hardware.py +186 -0
- oscura/acquisition/saleae.py +340 -0
- oscura/acquisition/socketcan.py +315 -0
- oscura/acquisition/streaming.py +38 -0
- oscura/acquisition/synthetic.py +229 -0
- oscura/acquisition/visa.py +376 -0
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/digital/__init__.py +48 -0
- oscura/analyzers/digital/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/extraction.py +195 -0
- oscura/analyzers/digital/ic_database.py +498 -0
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/digital/timing_paths.py +339 -0
- oscura/analyzers/digital/vintage.py +377 -0
- oscura/analyzers/digital/vintage_result.py +148 -0
- oscura/analyzers/protocols/__init__.py +22 -1
- oscura/analyzers/protocols/parallel_bus.py +449 -0
- oscura/analyzers/side_channel/__init__.py +52 -0
- oscura/analyzers/side_channel/power.py +690 -0
- oscura/analyzers/side_channel/timing.py +369 -0
- oscura/analyzers/signal_integrity/sparams.py +1 -1
- oscura/automotive/__init__.py +4 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/core/exceptions.py +5 -1
- oscura/export/__init__.py +12 -0
- oscura/export/wavedrom.py +430 -0
- oscura/exporters/json_export.py +47 -0
- oscura/exporters/vintage_logic_csv.py +247 -0
- oscura/loaders/__init__.py +1 -0
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/touchstone.py +1 -1
- oscura/reporting/__init__.py +7 -0
- oscura/reporting/vintage_logic_report.py +523 -0
- oscura/session/session.py +54 -46
- oscura/sessions/__init__.py +70 -0
- oscura/sessions/base.py +323 -0
- oscura/sessions/blackbox.py +640 -0
- oscura/sessions/generic.py +189 -0
- oscura/utils/autodetect.py +5 -1
- oscura/visualization/digital_advanced.py +718 -0
- oscura/visualization/figure_manager.py +156 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
- oscura/automotive/dtc/data.json +0 -2763
- oscura/schemas/bus_configuration.json +0 -322
- oscura/schemas/device_mapping.json +0 -182
- oscura/schemas/packet_format.json +0 -418
- oscura/schemas/protocol_definition.json +0 -363
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""Power analysis side-channel attacks.
|
|
2
|
+
|
|
3
|
+
This module implements Differential Power Analysis (DPA) and Correlation Power
|
|
4
|
+
Analysis (CPA) for extracting cryptographic keys from power consumption traces.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> import numpy as np
|
|
8
|
+
>>> from oscura.analyzers.side_channel.power import CPAAnalyzer
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Perform CPA attack on AES
|
|
11
|
+
>>> traces = np.array([...]) # Power traces (n_traces, n_samples)
|
|
12
|
+
>>> plaintexts = np.array([...]) # Known plaintexts
|
|
13
|
+
>>> cpa = CPAAnalyzer(leakage_model="hamming_weight", algorithm="aes_sbox")
|
|
14
|
+
>>> result = cpa.analyze(traces, plaintexts)
|
|
15
|
+
>>> print(f"Best key guess: 0x{result.key_guess:02X}")
|
|
16
|
+
|
|
17
|
+
References:
|
|
18
|
+
Kocher et al. "Differential Power Analysis" (CRYPTO 1999)
|
|
19
|
+
Brier et al. "Correlation Power Analysis with a Leakage Model" (CHES 2004)
|
|
20
|
+
Mangard et al. "Power Analysis Attacks" (Springer 2007)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from numpy.typing import NDArray
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"CPAAnalyzer",
|
|
37
|
+
"CPAResult",
|
|
38
|
+
"DPAAnalyzer",
|
|
39
|
+
"DPAResult",
|
|
40
|
+
"LeakageModel",
|
|
41
|
+
"hamming_distance",
|
|
42
|
+
"hamming_weight",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LeakageModel(Enum):
|
|
47
|
+
"""Power leakage models.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
HAMMING_WEIGHT: Count of '1' bits in value.
|
|
51
|
+
HAMMING_DISTANCE: Count of bit flips between states.
|
|
52
|
+
IDENTITY: Raw value (linear leakage).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
HAMMING_WEIGHT = "hamming_weight"
|
|
56
|
+
HAMMING_DISTANCE = "hamming_distance"
|
|
57
|
+
IDENTITY = "identity"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def hamming_weight(value: int | NDArray[np.integer[Any]]) -> int | NDArray[np.integer[Any]]:
|
|
61
|
+
"""Calculate Hamming weight (number of 1 bits).
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
value: Integer or array of integers.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Hamming weight(s).
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> hamming_weight(0x0F)
|
|
71
|
+
4
|
|
72
|
+
>>> hamming_weight(np.array([0x0F, 0xFF]))
|
|
73
|
+
array([4, 8])
|
|
74
|
+
"""
|
|
75
|
+
if isinstance(value, np.ndarray):
|
|
76
|
+
# Vectorized implementation for arrays
|
|
77
|
+
result = np.zeros(value.shape, dtype=np.int32)
|
|
78
|
+
temp_arr = value.astype(np.uint32)
|
|
79
|
+
while np.any(temp_arr):
|
|
80
|
+
result += temp_arr & 1
|
|
81
|
+
temp_arr >>= 1
|
|
82
|
+
return result
|
|
83
|
+
else:
|
|
84
|
+
# Scalar implementation
|
|
85
|
+
count: int = 0
|
|
86
|
+
temp: int = int(value)
|
|
87
|
+
while temp:
|
|
88
|
+
count += temp & 1
|
|
89
|
+
temp >>= 1
|
|
90
|
+
return count
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def hamming_distance(
|
|
94
|
+
val1: int | NDArray[np.integer[Any]], val2: int | NDArray[np.integer[Any]]
|
|
95
|
+
) -> int | NDArray[np.integer[Any]]:
|
|
96
|
+
"""Calculate Hamming distance (number of differing bits).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
val1: First value(s).
|
|
100
|
+
val2: Second value(s).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Hamming distance(s).
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> hamming_distance(0x00, 0xFF)
|
|
107
|
+
8
|
|
108
|
+
>>> hamming_distance(0x0F, 0xF0)
|
|
109
|
+
8
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(val1, np.ndarray) or isinstance(val2, np.ndarray):
|
|
112
|
+
v1 = np.asarray(val1)
|
|
113
|
+
v2 = np.asarray(val2)
|
|
114
|
+
return hamming_weight(np.bitwise_xor(v1, v2))
|
|
115
|
+
else:
|
|
116
|
+
return hamming_weight(val1 ^ val2)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# AES S-box for cryptographic modeling
|
|
120
|
+
AES_SBOX = np.array(
|
|
121
|
+
[
|
|
122
|
+
0x63,
|
|
123
|
+
0x7C,
|
|
124
|
+
0x77,
|
|
125
|
+
0x7B,
|
|
126
|
+
0xF2,
|
|
127
|
+
0x6B,
|
|
128
|
+
0x6F,
|
|
129
|
+
0xC5,
|
|
130
|
+
0x30,
|
|
131
|
+
0x01,
|
|
132
|
+
0x67,
|
|
133
|
+
0x2B,
|
|
134
|
+
0xFE,
|
|
135
|
+
0xD7,
|
|
136
|
+
0xAB,
|
|
137
|
+
0x76,
|
|
138
|
+
0xCA,
|
|
139
|
+
0x82,
|
|
140
|
+
0xC9,
|
|
141
|
+
0x7D,
|
|
142
|
+
0xFA,
|
|
143
|
+
0x59,
|
|
144
|
+
0x47,
|
|
145
|
+
0xF0,
|
|
146
|
+
0xAD,
|
|
147
|
+
0xD4,
|
|
148
|
+
0xA2,
|
|
149
|
+
0xAF,
|
|
150
|
+
0x9C,
|
|
151
|
+
0xA4,
|
|
152
|
+
0x72,
|
|
153
|
+
0xC0,
|
|
154
|
+
0xB7,
|
|
155
|
+
0xFD,
|
|
156
|
+
0x93,
|
|
157
|
+
0x26,
|
|
158
|
+
0x36,
|
|
159
|
+
0x3F,
|
|
160
|
+
0xF7,
|
|
161
|
+
0xCC,
|
|
162
|
+
0x34,
|
|
163
|
+
0xA5,
|
|
164
|
+
0xE5,
|
|
165
|
+
0xF1,
|
|
166
|
+
0x71,
|
|
167
|
+
0xD8,
|
|
168
|
+
0x31,
|
|
169
|
+
0x15,
|
|
170
|
+
0x04,
|
|
171
|
+
0xC7,
|
|
172
|
+
0x23,
|
|
173
|
+
0xC3,
|
|
174
|
+
0x18,
|
|
175
|
+
0x96,
|
|
176
|
+
0x05,
|
|
177
|
+
0x9A,
|
|
178
|
+
0x07,
|
|
179
|
+
0x12,
|
|
180
|
+
0x80,
|
|
181
|
+
0xE2,
|
|
182
|
+
0xEB,
|
|
183
|
+
0x27,
|
|
184
|
+
0xB2,
|
|
185
|
+
0x75,
|
|
186
|
+
0x09,
|
|
187
|
+
0x83,
|
|
188
|
+
0x2C,
|
|
189
|
+
0x1A,
|
|
190
|
+
0x1B,
|
|
191
|
+
0x6E,
|
|
192
|
+
0x5A,
|
|
193
|
+
0xA0,
|
|
194
|
+
0x52,
|
|
195
|
+
0x3B,
|
|
196
|
+
0xD6,
|
|
197
|
+
0xB3,
|
|
198
|
+
0x29,
|
|
199
|
+
0xE3,
|
|
200
|
+
0x2F,
|
|
201
|
+
0x84,
|
|
202
|
+
0x53,
|
|
203
|
+
0xD1,
|
|
204
|
+
0x00,
|
|
205
|
+
0xED,
|
|
206
|
+
0x20,
|
|
207
|
+
0xFC,
|
|
208
|
+
0xB1,
|
|
209
|
+
0x5B,
|
|
210
|
+
0x6A,
|
|
211
|
+
0xCB,
|
|
212
|
+
0xBE,
|
|
213
|
+
0x39,
|
|
214
|
+
0x4A,
|
|
215
|
+
0x4C,
|
|
216
|
+
0x58,
|
|
217
|
+
0xCF,
|
|
218
|
+
0xD0,
|
|
219
|
+
0xEF,
|
|
220
|
+
0xAA,
|
|
221
|
+
0xFB,
|
|
222
|
+
0x43,
|
|
223
|
+
0x4D,
|
|
224
|
+
0x33,
|
|
225
|
+
0x85,
|
|
226
|
+
0x45,
|
|
227
|
+
0xF9,
|
|
228
|
+
0x02,
|
|
229
|
+
0x7F,
|
|
230
|
+
0x50,
|
|
231
|
+
0x3C,
|
|
232
|
+
0x9F,
|
|
233
|
+
0xA8,
|
|
234
|
+
0x51,
|
|
235
|
+
0xA3,
|
|
236
|
+
0x40,
|
|
237
|
+
0x8F,
|
|
238
|
+
0x92,
|
|
239
|
+
0x9D,
|
|
240
|
+
0x38,
|
|
241
|
+
0xF5,
|
|
242
|
+
0xBC,
|
|
243
|
+
0xB6,
|
|
244
|
+
0xDA,
|
|
245
|
+
0x21,
|
|
246
|
+
0x10,
|
|
247
|
+
0xFF,
|
|
248
|
+
0xF3,
|
|
249
|
+
0xD2,
|
|
250
|
+
0xCD,
|
|
251
|
+
0x0C,
|
|
252
|
+
0x13,
|
|
253
|
+
0xEC,
|
|
254
|
+
0x5F,
|
|
255
|
+
0x97,
|
|
256
|
+
0x44,
|
|
257
|
+
0x17,
|
|
258
|
+
0xC4,
|
|
259
|
+
0xA7,
|
|
260
|
+
0x7E,
|
|
261
|
+
0x3D,
|
|
262
|
+
0x64,
|
|
263
|
+
0x5D,
|
|
264
|
+
0x19,
|
|
265
|
+
0x73,
|
|
266
|
+
0x60,
|
|
267
|
+
0x81,
|
|
268
|
+
0x4F,
|
|
269
|
+
0xDC,
|
|
270
|
+
0x22,
|
|
271
|
+
0x2A,
|
|
272
|
+
0x90,
|
|
273
|
+
0x88,
|
|
274
|
+
0x46,
|
|
275
|
+
0xEE,
|
|
276
|
+
0xB8,
|
|
277
|
+
0x14,
|
|
278
|
+
0xDE,
|
|
279
|
+
0x5E,
|
|
280
|
+
0x0B,
|
|
281
|
+
0xDB,
|
|
282
|
+
0xE0,
|
|
283
|
+
0x32,
|
|
284
|
+
0x3A,
|
|
285
|
+
0x0A,
|
|
286
|
+
0x49,
|
|
287
|
+
0x06,
|
|
288
|
+
0x24,
|
|
289
|
+
0x5C,
|
|
290
|
+
0xC2,
|
|
291
|
+
0xD3,
|
|
292
|
+
0xAC,
|
|
293
|
+
0x62,
|
|
294
|
+
0x91,
|
|
295
|
+
0x95,
|
|
296
|
+
0xE4,
|
|
297
|
+
0x79,
|
|
298
|
+
0xE7,
|
|
299
|
+
0xC8,
|
|
300
|
+
0x37,
|
|
301
|
+
0x6D,
|
|
302
|
+
0x8D,
|
|
303
|
+
0xD5,
|
|
304
|
+
0x4E,
|
|
305
|
+
0xA9,
|
|
306
|
+
0x6C,
|
|
307
|
+
0x56,
|
|
308
|
+
0xF4,
|
|
309
|
+
0xEA,
|
|
310
|
+
0x65,
|
|
311
|
+
0x7A,
|
|
312
|
+
0xAE,
|
|
313
|
+
0x08,
|
|
314
|
+
0xBA,
|
|
315
|
+
0x78,
|
|
316
|
+
0x25,
|
|
317
|
+
0x2E,
|
|
318
|
+
0x1C,
|
|
319
|
+
0xA6,
|
|
320
|
+
0xB4,
|
|
321
|
+
0xC6,
|
|
322
|
+
0xE8,
|
|
323
|
+
0xDD,
|
|
324
|
+
0x74,
|
|
325
|
+
0x1F,
|
|
326
|
+
0x4B,
|
|
327
|
+
0xBD,
|
|
328
|
+
0x8B,
|
|
329
|
+
0x8A,
|
|
330
|
+
0x70,
|
|
331
|
+
0x3E,
|
|
332
|
+
0xB5,
|
|
333
|
+
0x66,
|
|
334
|
+
0x48,
|
|
335
|
+
0x03,
|
|
336
|
+
0xF6,
|
|
337
|
+
0x0E,
|
|
338
|
+
0x61,
|
|
339
|
+
0x35,
|
|
340
|
+
0x57,
|
|
341
|
+
0xB9,
|
|
342
|
+
0x86,
|
|
343
|
+
0xC1,
|
|
344
|
+
0x1D,
|
|
345
|
+
0x9E,
|
|
346
|
+
0xE1,
|
|
347
|
+
0xF8,
|
|
348
|
+
0x98,
|
|
349
|
+
0x11,
|
|
350
|
+
0x69,
|
|
351
|
+
0xD9,
|
|
352
|
+
0x8E,
|
|
353
|
+
0x94,
|
|
354
|
+
0x9B,
|
|
355
|
+
0x1E,
|
|
356
|
+
0x87,
|
|
357
|
+
0xE9,
|
|
358
|
+
0xCE,
|
|
359
|
+
0x55,
|
|
360
|
+
0x28,
|
|
361
|
+
0xDF,
|
|
362
|
+
0x8C,
|
|
363
|
+
0xA1,
|
|
364
|
+
0x89,
|
|
365
|
+
0x0D,
|
|
366
|
+
0xBF,
|
|
367
|
+
0xE6,
|
|
368
|
+
0x42,
|
|
369
|
+
0x68,
|
|
370
|
+
0x41,
|
|
371
|
+
0x99,
|
|
372
|
+
0x2D,
|
|
373
|
+
0x0F,
|
|
374
|
+
0xB0,
|
|
375
|
+
0x54,
|
|
376
|
+
0xBB,
|
|
377
|
+
0x16,
|
|
378
|
+
],
|
|
379
|
+
dtype=np.uint8,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@dataclass
|
|
384
|
+
class DPAResult:
|
|
385
|
+
"""Result of Differential Power Analysis attack.
|
|
386
|
+
|
|
387
|
+
Attributes:
|
|
388
|
+
key_guess: Most likely key byte value.
|
|
389
|
+
differential_traces: Differential traces for each key hypothesis (256, n_samples).
|
|
390
|
+
max_differential: Maximum differential value achieved.
|
|
391
|
+
key_rank: Ranking of all key hypotheses by differential.
|
|
392
|
+
peak_sample: Sample index where maximum differential occurs.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
key_guess: int
|
|
396
|
+
differential_traces: NDArray[np.floating[Any]]
|
|
397
|
+
max_differential: float
|
|
398
|
+
key_rank: NDArray[np.integer[Any]]
|
|
399
|
+
peak_sample: int
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@dataclass
|
|
403
|
+
class CPAResult:
|
|
404
|
+
"""Result of Correlation Power Analysis attack.
|
|
405
|
+
|
|
406
|
+
Attributes:
|
|
407
|
+
key_guess: Most likely key byte value.
|
|
408
|
+
max_correlation: Maximum correlation coefficient achieved.
|
|
409
|
+
correlations: Correlation values for each key hypothesis (256, n_samples).
|
|
410
|
+
key_rank: Ranking of all key hypotheses by correlation.
|
|
411
|
+
peak_sample: Sample index where maximum correlation occurs.
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
key_guess: int
|
|
415
|
+
max_correlation: float
|
|
416
|
+
correlations: NDArray[np.floating[Any]]
|
|
417
|
+
key_rank: NDArray[np.integer[Any]]
|
|
418
|
+
peak_sample: int
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class DPAAnalyzer:
|
|
422
|
+
"""Differential Power Analysis (DPA) attack implementation.
|
|
423
|
+
|
|
424
|
+
DPA exploits differences in power consumption based on data-dependent
|
|
425
|
+
operations. Traces are partitioned by a selection function and averaged
|
|
426
|
+
to reveal key-dependent differences.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
target_bit: Bit position to target (0-7) for AES S-box output.
|
|
430
|
+
byte_position: Byte position in key (0-15 for AES-128).
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
>>> dpa = DPAAnalyzer(target_bit=0, byte_position=0)
|
|
434
|
+
>>> result = dpa.analyze(power_traces, known_plaintexts)
|
|
435
|
+
>>> print(f"Key byte 0: 0x{result.key_guess:02X}")
|
|
436
|
+
|
|
437
|
+
References:
|
|
438
|
+
Kocher et al. "Differential Power Analysis" CRYPTO 1999
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
def __init__(self, target_bit: int = 0, byte_position: int = 0) -> None:
|
|
442
|
+
"""Initialize DPA analyzer.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
target_bit: Target bit position (0-7).
|
|
446
|
+
byte_position: Key byte position (0-15).
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
ValueError: If parameters out of range.
|
|
450
|
+
"""
|
|
451
|
+
if not 0 <= target_bit <= 7:
|
|
452
|
+
raise ValueError(f"target_bit must be 0-7, got {target_bit}")
|
|
453
|
+
if not 0 <= byte_position <= 15:
|
|
454
|
+
raise ValueError(f"byte_position must be 0-15, got {byte_position}")
|
|
455
|
+
|
|
456
|
+
self.target_bit = target_bit
|
|
457
|
+
self.byte_position = byte_position
|
|
458
|
+
|
|
459
|
+
def _selection_function(self, plaintext_byte: int, key_guess: int) -> int:
|
|
460
|
+
"""Selection function: bit value of S-box output.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
plaintext_byte: Input plaintext byte.
|
|
464
|
+
key_guess: Hypothetical key byte.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Value of target bit (0 or 1).
|
|
468
|
+
"""
|
|
469
|
+
sbox_out = AES_SBOX[plaintext_byte ^ key_guess]
|
|
470
|
+
return int((sbox_out >> self.target_bit) & 1)
|
|
471
|
+
|
|
472
|
+
def analyze(
|
|
473
|
+
self,
|
|
474
|
+
traces: NDArray[np.floating[Any]],
|
|
475
|
+
plaintexts: NDArray[np.integer[Any]],
|
|
476
|
+
) -> DPAResult:
|
|
477
|
+
"""Perform DPA attack to recover key byte.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
traces: Power traces (n_traces, n_samples).
|
|
481
|
+
plaintexts: Known plaintexts (n_traces, 16) or (n_traces,) if single byte.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
DPAResult with key guess and differential traces.
|
|
485
|
+
|
|
486
|
+
Raises:
|
|
487
|
+
ValueError: If input shapes incompatible.
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
>>> traces = np.random.randn(1000, 5000) # 1000 traces, 5000 samples
|
|
491
|
+
>>> plaintexts = np.random.randint(0, 256, (1000, 16), dtype=np.uint8)
|
|
492
|
+
>>> result = dpa.analyze(traces, plaintexts)
|
|
493
|
+
"""
|
|
494
|
+
n_traces, n_samples = traces.shape
|
|
495
|
+
|
|
496
|
+
# Extract target byte from plaintexts
|
|
497
|
+
if plaintexts.ndim == 1:
|
|
498
|
+
plaintext_bytes = plaintexts
|
|
499
|
+
elif plaintexts.ndim == 2:
|
|
500
|
+
plaintext_bytes = plaintexts[:, self.byte_position]
|
|
501
|
+
else:
|
|
502
|
+
raise ValueError(f"plaintexts must be 1D or 2D, got shape {plaintexts.shape}")
|
|
503
|
+
|
|
504
|
+
if len(plaintext_bytes) != n_traces:
|
|
505
|
+
raise ValueError(
|
|
506
|
+
f"Number of plaintexts ({len(plaintext_bytes)}) must match traces ({n_traces})"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Calculate differential for each key hypothesis
|
|
510
|
+
differential_traces = np.zeros((256, n_samples), dtype=np.float64)
|
|
511
|
+
|
|
512
|
+
for key_guess in range(256):
|
|
513
|
+
# Partition traces by selection function
|
|
514
|
+
selection_bits = np.array(
|
|
515
|
+
[self._selection_function(pt, key_guess) for pt in plaintext_bytes]
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
set_0 = traces[selection_bits == 0]
|
|
519
|
+
set_1 = traces[selection_bits == 1]
|
|
520
|
+
|
|
521
|
+
if len(set_0) > 0 and len(set_1) > 0:
|
|
522
|
+
# Differential = mean(set_1) - mean(set_0)
|
|
523
|
+
differential_traces[key_guess] = np.mean(set_1, axis=0) - np.mean(set_0, axis=0)
|
|
524
|
+
|
|
525
|
+
# Find key with maximum differential
|
|
526
|
+
max_differentials = np.max(np.abs(differential_traces), axis=1)
|
|
527
|
+
key_rank = np.argsort(max_differentials)[::-1] # Descending order
|
|
528
|
+
key_guess = key_rank[0]
|
|
529
|
+
max_differential = max_differentials[key_guess]
|
|
530
|
+
peak_sample = int(np.argmax(np.abs(differential_traces[key_guess])))
|
|
531
|
+
|
|
532
|
+
return DPAResult(
|
|
533
|
+
key_guess=int(key_guess),
|
|
534
|
+
differential_traces=differential_traces,
|
|
535
|
+
max_differential=float(max_differential),
|
|
536
|
+
key_rank=key_rank,
|
|
537
|
+
peak_sample=peak_sample,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
class CPAAnalyzer:
|
|
542
|
+
"""Correlation Power Analysis (CPA) attack implementation.
|
|
543
|
+
|
|
544
|
+
CPA uses statistical correlation between power consumption and
|
|
545
|
+
intermediate values predicted by a leakage model.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
leakage_model: Leakage model ("hamming_weight", "hamming_distance", "identity").
|
|
549
|
+
algorithm: Target algorithm ("aes_sbox", "des", "custom").
|
|
550
|
+
byte_position: Key byte position to attack (0-15).
|
|
551
|
+
|
|
552
|
+
Example:
|
|
553
|
+
>>> cpa = CPAAnalyzer(leakage_model="hamming_weight", algorithm="aes_sbox")
|
|
554
|
+
>>> result = cpa.analyze(power_traces, known_plaintexts)
|
|
555
|
+
>>> print(f"Correlation: {result.max_correlation:.4f}")
|
|
556
|
+
|
|
557
|
+
References:
|
|
558
|
+
Brier et al. "Correlation Power Analysis" CHES 2004
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
def __init__(
|
|
562
|
+
self,
|
|
563
|
+
leakage_model: Literal["hamming_weight", "hamming_distance", "identity"] = "hamming_weight",
|
|
564
|
+
algorithm: Literal["aes_sbox", "des", "custom"] = "aes_sbox",
|
|
565
|
+
byte_position: int = 0,
|
|
566
|
+
) -> None:
|
|
567
|
+
"""Initialize CPA analyzer.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
leakage_model: Power leakage model.
|
|
571
|
+
algorithm: Target cryptographic algorithm.
|
|
572
|
+
byte_position: Target key byte position.
|
|
573
|
+
|
|
574
|
+
Raises:
|
|
575
|
+
ValueError: If parameters invalid.
|
|
576
|
+
"""
|
|
577
|
+
valid_models = ["hamming_weight", "hamming_distance", "identity"]
|
|
578
|
+
if leakage_model not in valid_models:
|
|
579
|
+
raise ValueError(f"leakage_model must be one of {valid_models}")
|
|
580
|
+
|
|
581
|
+
if not 0 <= byte_position <= 15:
|
|
582
|
+
raise ValueError(f"byte_position must be 0-15, got {byte_position}")
|
|
583
|
+
|
|
584
|
+
self.leakage_model = leakage_model
|
|
585
|
+
self.algorithm = algorithm
|
|
586
|
+
self.byte_position = byte_position
|
|
587
|
+
|
|
588
|
+
# Select leakage function
|
|
589
|
+
if leakage_model == "hamming_weight":
|
|
590
|
+
self._leakage_func: Callable[[NDArray[np.integer[Any]]], NDArray[np.integer[Any]]] = (
|
|
591
|
+
hamming_weight # type: ignore[assignment]
|
|
592
|
+
)
|
|
593
|
+
elif leakage_model == "identity":
|
|
594
|
+
self._leakage_func = lambda x: x # Identity function cannot be simplified
|
|
595
|
+
else:
|
|
596
|
+
self._leakage_func = hamming_weight # type: ignore[assignment]
|
|
597
|
+
|
|
598
|
+
def _compute_intermediate(
|
|
599
|
+
self, plaintext_byte: NDArray[np.integer[Any]], key_guess: int
|
|
600
|
+
) -> NDArray[np.integer[Any]]:
|
|
601
|
+
"""Compute intermediate value for key hypothesis.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
plaintext_byte: Plaintext byte values.
|
|
605
|
+
key_guess: Hypothetical key byte.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Intermediate values (e.g., S-box output).
|
|
609
|
+
"""
|
|
610
|
+
if self.algorithm == "aes_sbox":
|
|
611
|
+
result: NDArray[np.integer[Any]] = AES_SBOX[plaintext_byte ^ key_guess]
|
|
612
|
+
return result
|
|
613
|
+
else:
|
|
614
|
+
# Default: XOR with key
|
|
615
|
+
return plaintext_byte ^ key_guess
|
|
616
|
+
|
|
617
|
+
def analyze(
|
|
618
|
+
self,
|
|
619
|
+
traces: NDArray[np.floating[Any]],
|
|
620
|
+
plaintexts: NDArray[np.integer[Any]],
|
|
621
|
+
) -> CPAResult:
|
|
622
|
+
"""Perform CPA attack to recover key byte.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
traces: Power traces (n_traces, n_samples).
|
|
626
|
+
plaintexts: Known plaintexts (n_traces, 16) or (n_traces,).
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
CPAResult with key guess and correlation matrix.
|
|
630
|
+
|
|
631
|
+
Raises:
|
|
632
|
+
ValueError: If input shapes incompatible.
|
|
633
|
+
|
|
634
|
+
Example:
|
|
635
|
+
>>> traces = np.random.randn(1000, 5000)
|
|
636
|
+
>>> plaintexts = np.random.randint(0, 256, (1000, 16), dtype=np.uint8)
|
|
637
|
+
>>> result = cpa.analyze(traces, plaintexts)
|
|
638
|
+
>>> print(f"Best key: 0x{result.key_guess:02X}")
|
|
639
|
+
"""
|
|
640
|
+
n_traces, n_samples = traces.shape
|
|
641
|
+
|
|
642
|
+
# Extract target byte
|
|
643
|
+
if plaintexts.ndim == 1:
|
|
644
|
+
plaintext_bytes = plaintexts
|
|
645
|
+
elif plaintexts.ndim == 2:
|
|
646
|
+
plaintext_bytes = plaintexts[:, self.byte_position]
|
|
647
|
+
else:
|
|
648
|
+
raise ValueError(f"plaintexts must be 1D or 2D, got shape {plaintexts.shape}")
|
|
649
|
+
|
|
650
|
+
if len(plaintext_bytes) != n_traces:
|
|
651
|
+
raise ValueError(
|
|
652
|
+
f"Number of plaintexts ({len(plaintext_bytes)}) must match traces ({n_traces})"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# Compute correlations for all key hypotheses
|
|
656
|
+
correlations = np.zeros((256, n_samples), dtype=np.float64)
|
|
657
|
+
|
|
658
|
+
for key_guess in range(256):
|
|
659
|
+
# Compute intermediate values
|
|
660
|
+
intermediates = self._compute_intermediate(plaintext_bytes, key_guess)
|
|
661
|
+
|
|
662
|
+
# Apply leakage model
|
|
663
|
+
hypothetical_power = self._leakage_func(intermediates).astype(np.float64)
|
|
664
|
+
|
|
665
|
+
# Compute Pearson correlation for each sample point
|
|
666
|
+
for sample_idx in range(n_samples):
|
|
667
|
+
trace_sample = traces[:, sample_idx]
|
|
668
|
+
|
|
669
|
+
# Pearson correlation coefficient
|
|
670
|
+
correlations[key_guess, sample_idx] = np.corrcoef(hypothetical_power, trace_sample)[
|
|
671
|
+
0, 1
|
|
672
|
+
]
|
|
673
|
+
|
|
674
|
+
# Handle NaN values (can occur with constant traces)
|
|
675
|
+
correlations = np.nan_to_num(correlations, nan=0.0)
|
|
676
|
+
|
|
677
|
+
# Find key with maximum absolute correlation
|
|
678
|
+
max_correlations = np.max(np.abs(correlations), axis=1)
|
|
679
|
+
key_rank = np.argsort(max_correlations)[::-1]
|
|
680
|
+
key_guess = key_rank[0]
|
|
681
|
+
max_correlation = max_correlations[key_guess]
|
|
682
|
+
peak_sample = int(np.argmax(np.abs(correlations[key_guess])))
|
|
683
|
+
|
|
684
|
+
return CPAResult(
|
|
685
|
+
key_guess=int(key_guess),
|
|
686
|
+
max_correlation=float(max_correlation),
|
|
687
|
+
correlations=correlations,
|
|
688
|
+
key_rank=key_rank,
|
|
689
|
+
peak_sample=peak_sample,
|
|
690
|
+
)
|