Inconsistent Ciphertext When Using Different Communication Methods with CW305 AES

Hello,

I am trying to perform clock glitching on the AES example provided with ChipWhisperer, but I’ve encountered some issues with how the cipher is configured. Specifically, I’m testing three different ways to set the key and plaintext, then read the ciphertext produced by the target. However, I get different ciphertexts even when using the same key and plaintext, depending on the method used.

Option 1: Using cw.capture_trace()

ktp = cw.ktp.Basic()
ktp.fixed_key = True   
ktp.fixed_text = True   
key, text = ktp.next()

trace = cw.capture_trace(scope, target, text, key, ack=True, poll_done=False, as_int=False, always_send_key=False )

print("Plain-text:", text.hex())
print("Key:", key.hex())

wave = trace.wave
cipher = trace.textout 
print("Ciphertext:", cipher.hex())

Output:

Plain-text: 000102030405060708090a0b0c0d0e0f
Key: 2b7e151628aed2a6abf7158809cf4f3c
Ciphertext: 50fe67cc996d32b6da0937e99bafec60

Option 2: Writing Directly to FPGA Registers
Using the register definitions from cw305_aes_defines.v in the Vivado project cw305_aes.xpr:

ktp = cw.ktp.Basic()
ktp.fixed_key = True 
ktp.fixed_text = True  
key, text = ktp.next()

target.fpga_write(target.REG_CRYPT_TEXTIN, text)
target.fpga_write(target.REG_CRYPT_KEY, key)
target.fpga_write(target.REG_CRYPT_GO, [1])
cipher_bytes = target.fpga_read(target.REG_CRYPT_CIPHEROUT, 16)
cipher = bytes(cipher_bytes)

print("Plain-text:", text.hex())
print("Key:", key.hex())
print("Ciphertext:", cipher.hex())

Output:

Plain-text: 000102030405060708090a0b0c0d0e0f
Key: 2b7e151628aed2a6abf7158809cf4f3c
Ciphertext: 3101356e05bbc0f59246f8af9f3f4be6

As you can see, the plaintext and key are exactly the same, but the ciphertext differs from Option 1.

Option 3: Running the Basic AES Example

I also ran the basic AES example for the CW305 board using the same key and plaintext:

from tqdm.notebook import tnrange
import numpy as np
import time
from Crypto.Cipher import AES

ktp = cw.ktp.Basic()

traces = []
textin = []
keys = []
N = 5000  # Number of traces

# initialize cipher to verify DUT result:
cipher = AES.new(bytes(key), AES.MODE_ECB)

print("Plain-text:", text.hex())
print("Key:", key.hex())

textin.append(text)
keys.append(key)

ret = cw.capture_trace(scope, target, text, key)
ciphertext = trace.textout
print("Ciphertext:", ciphertext.hex())

Output:

Plain-text: 000102030405060708090a0b0c0d0e0f
Key: 2b7e151628aed2a6abf7158809cf4f3c
Ciphertext: 50fe67cc996d32b6da0937e99bafec60

This matches the result from Option 1, confirming that this is the correct ciphertext.

My Issue

I am performing a clock glitching attack and want to compare the obtained ciphertext with the expected result. I prefer using Option 2 (direct register access), as it directly interfaces with the modified AES core in the Vivado project, making it easier to substitute another circuit in the future.

However, since Option 2 produces a different ciphertext even without glitches, I can’t reliably determine if the glitch had any effect. This breaks my glitching loop logic.

Glitching Loop (Simplified):

import struct
import numpy as np
from tqdm import trange


def reboot_flush():
    scope.io.nrst = False
    time.sleep(0.05)
    scope.io.nrst = "high_z"
    time.sleep(0.05)

    #Flush garbage too
    target.flush()
    
glitch_values   = [
    (1.0, 0.5),
    (1.0, 1.0),
    (2.0, 5.0),
    (3.0, 8.0),
]
rounds_per_point = 5

glitch_outputs = []
print("Running selected glitches")
print("Expected sum:", expected_sum)
print("Plain-text:", text.hex())
print("Key:", key.hex())

for w, off in glitch_values:

    scope.glitch.width = w
    scope.glitch.ext_offset = off


    result = {"width": w, "offset": off, "resets": 0, "successes": []}
    glitch_outputs.append(result)

    for _ in range(rounds_per_point):

        if scope.adc.state:
            result["resets"] += 1
            reboot_flush()


        scope.arm()

        target.fpga_write(target.REG_CRYPT_TEXTIN, text)
        target.fpga_write(target.REG_CRYPT_KEY, key)

        target.fpga_write(target.REG_CRYPT_GO, [1])

        ret = scope.capture()

        cipher_bytes = target.fpga_read(target.REG_CRYPT_CIPHEROUT, 16)
        cipher = bytes(cipher_bytes)

        if ret:  # timeout → no llegó trigger
            result["resets"] += 1
            reboot_flush()
        else:
            # Comparamos la suma con el valor esperado
            output_sum = sum(cipher[i] for i in range(16))
            if output_sum != expected_sum:
                result["successes"].append(output_sum)
                print(f"Successful glitch: sum={output_sum}, setting=(w={w}, off={off})")
                print("    Ciphertext:", cipher.hex())


This is also not working as I am getting the same ciphertext as in Option 1, meaning that the glitches are having no effect on the AES execution. It appears the design is not reacting to the glitch attempts at all.

My objective is to perform clock glitch attacks on my own hardware designs using the CW305 target board and the CW1173 ChipWhisperer-Lite capture board.

My Questions:

  1. Why do Options 1 and 3 produce the correct ciphertext, but Option 2 doesn’t, even though the key and plaintext are identical?
  2. Is there a known difference in how cw.capture_trace() and direct FPGA register access trigger or handle the AES operation?
  3. Could it be a timing or synchronization issue between writing to the registers and reading the output?

Any insight or suggestions are highly appreciated. Thank you!

PD: This is the Jupyter Notebook where i performed all the aforementioned tests:
Tests_AES_CW305.zip (14.8 KB)

1, 2 and 3 are the same - if you follow the code to see what happens when you call cw.capture_trace(), you find calls to fpga_write() for these same registers. What you’ve missed is that in these calls, we reverse the order of bytes that are written (and read back); see for example: chipwhisperer/software/chipwhisperer/capture/targets/CW305.py at develop · newaetech/chipwhisperer · GitHub

Thanks for the clarification, that was really helpful