Help Debugging Custom CPA Leakage Model

Hi everyone,

I’m working on a CPA attack against a hardware implementation of the PRESENT cipher using ChipWhisperer. I’ve successfully captured traces and saved them to a project.

My problem is that when I run the CPA, I’m recovering the wrong key. This makes me confident my leakage model is incorrect, but I’m not sure what to try next.

My current model is a simple, first-round attack that assumes a Hamming Weight (HW) leak on the output of the S-box.

# PRESENT S-box
sbox = [0xC, 0x5, 0x6, 0xB, 0x9, 0x0, 0xA, 0xD, 
        0x3, 0xE, 0xF, 0x8, 0x4, 0x7, 0x1, 0x2]

class PresentModel:
    def __init__(self):
        self.sbox = sbox
        self._has_prev = False
    
    def getNumSubKeys(self): 
        return 16
    
    def getPermPerSubkey(self): 
        return 256
    
    def leakage(self, pt, ct, key, bnum, state):
        # Convert plaintext to int
        if not isinstance(pt, int):
            pt = int.from_bytes(bytes(pt[:8]), 'big')
            
        # Isolate the target nibble
        nibble = (pt >> (bnum * 4)) & 0xF
        
        # XOR with key guess (masked to 4 bits)
        after_key = nibble ^ (key & 0xF)
        
        # S-box lookup
        sbox_out = self.sbox[after_key]
        
        # My hypothesis: Hamming Weight of the S-box output
        return bin(sbox_out).count('1')

leak_model = PresentModel()
attack = cwa.cpa(project, leak_model)
# ... (run attack) ...
results = attack.run()
key_bytes = results.key_guess()
# ... (format key) ...
print(f"Recovered key: {key_hex}") # Prints the wrong key

The main things that come to mind are:

  1. Are you 100% sure that your traces include the first round?
  2. You’re recovering the wrong key but is it just a matter of not enough traces? In our notebooks we compute the PGE to see how far away the key guess is from the actual key. You could have the wrong key but if the correct key bytes are not too far behind…
  3. Are you sure your model is appropriate for the implementation? Is the sbox output stored to a register? Our pipelined AES demo delves into matching the leakage model to the implementation. Even if you store the sbox output in flops in your source code, are you sure that your FPGA implementation tool kept it like that? If you use retiming, it might not.

This was how the traces were taken

from tqdm.notebook import tnrange
import numpy as np
import time
import csv
import os

ktp = cw.ktp.Basic()

traces = []
textin = []
keys = []
N = 30000  

plaintexts_csv = "plaintexts.csv"
traces_csv = "traces.csv"

key_fixed = bytearray([0x00] * 16)

with open(plaintexts_csv, 'w', newline='') as f_pt:
    writer_pt = csv.writer(f_pt)

with open(traces_csv, 'w', newline='') as f_tr:
    writer_tr = csv.writer(f_tr)

for i in tnrange(N, desc='Capturing traces'):

    # --- Minimal change: random 64-bit plaintext (8 bytes) ---
    text = bytearray(np.random.randint(0, 256, 8).tolist())

    # store metadata
    textin.append(text)
    keys.append(key_fixed)
    
    # Append plaintext to CSV 
    with open(plaintexts_csv, 'a', newline='') as f_pt:
        writer_pt = csv.writer(f_pt)
        writer_pt.writerow(list(text))
    
    ret = cw.capture_trace(scope, target, text, key_fixed)
    if not ret:
        print("Failed capture")
        continue

    # save waveform and full capture
    traces.append(ret.wave)
    project.traces.append(ret)
    
    with open(traces_csv, 'a', newline='') as f_tr:
        writer_tr = csv.writer(f_tr)

How can i make sure the first round is measured?

That is something that you need to figure out, because it’s your target implementation. You could run a simulation and observe when the first round is completed relative to when the trigger line goes high.

for reference, this is the trace for aes recorded on a CW310, it doesnt look right?

For starters, your gain is much too low.