Chipwhisperer CW305に実装したAES回路にCPAを行う公式チュートリアルをなぞる

# Attacking AES on FPGA (CW305 / Artix7 XC7A100T)

(2022/05/07 ChipWhisperer5.6.0 に対応するよう更新)

FPGAに実装したAESに対してCPAする．

- [API reference](https://chipwhisperer.readthedocs.io/en/latest/api.html)
- [Existing tutorial](http://wiki.newae.com/Tutorial_CW305-1_Building_a_Project#Capture_Setup)
- [元ソース (Github)](https://github.com/newaetech/chipwhisperer-jupyter/blob/0e0ee9ee52c9141f735fdc31bfc2065393596174/demos/PA_HW_CW305_1-Attacking_AES_on_an_FPGA.ipynb)

## 注意
現時点でLatestとしてタグ付けされているv5.5.2の場合，WindowsでCW-LiteとCW305を同時にUSBで繋ぐと`cw.scope()`した際にCW-Liteにつながらない問題が発生する．
原因は不明だが，回避できそうな方法のメモを最後に記載している．

v5.6.0では修正(というよりUSBライブラリの変更に関連してコードが一通りリファクタリング？)されているため，Pre-releaseではあるがv5.6.0を使用するか，この問題が発生する前のバージョン(おそらくv5.3.1かそれよりも古いもの)を使用するのがよい(と思われるが，Pre-Releaseなので他のバグが発生する可能性がある)．

2020年11月以降にリリースされたファームウェアでは，ChipwhispererはWindows上でPlug-and-Playに対応しているためドライバのインストールは不要である．
これ以前のものを使用している場合，ドライバを手動で適用する必要がある．([参考リンク](https://chipwhisperer.readthedocs.io/en/latest/drivers.html))

## 動作確認済み環境

- Windows10 Home (21H2/19044.1645)
- ChipWhisperer-Lite [(Mouser)](https://www.mouser.jp/ProductDetail/NewAE/NAE-CW1173-2PART?qs=sGAEpiMZZMu3sxpa5v1qrvyz3TtLhlht12Ked44sfeQ%3D)
- CW305 Artix7 FPGA Target (XC7A100T-2FTG256) [(Mouser)](https://www.mouser.jp/ProductDetail/NewAE/NAE-CW305-04-7A100-010-X?sqs=r5DSvlrkXmLRAqM1gSUr%252BQ%3D%3D)
- ChipWhisperer 5.6.0

なお，Windowsのサポートはあまり熱心にされていないのでLinux上でGitHubから最新のものをcloneしてきて動かすほうが楽である．

## インストールと接続

1. ChipWhispererのGitHubからインストーラをダウンロード
  今回は[ChipWhisperer5.6.0](https://github.com/newaetech/chipwhisperer/releases/tag/BHUSA2021)，Windowsの場合は`Chipwhisperer.v5.6.0.Setup.64-bit.exe`をダウンロードしてインストール．

1. 公式の指示通りにCW305とChipWhisperer-Liteを接続する．

1. PCにChipWhisperer-LiteとCW305の両方をUSBケーブルで接続する．

<img src='http://wiki.newae.com/images/thumb/7/72/CW305_CWLite_SCA.jpg/800x712x800px-CW305_CWLite_SCA.jpg.pagespeed.ic.EOOE6aopWs.webp' width='500'>

## 動かす

ChipWhispererを起動すると，ブラウザ上でJupyter Notebookが開かれる．
適当に作業用フォルダを設定し，空のNotebookを作ったら以下のプログラムを順にNotebookのセル上にコピペして動作させる．

### 必要なライブラリのインポート
```python
import chipwhisperer as cw
import os
```

### Chipwhisperer-Liteの設定

```python
scope = cw.scope()

scope.gain.db = 25
scope.adc.samples = 129
scope.adc.offset = 0
scope.adc.basic_mode = "rising_edge"
scope.clock.clkgen_freq = 7370000
scope.clock.adc_src = "extclk_x4"
scope.trigger.triggers = "tio4"
scope.io.tio1 = "serial_rx"
scope.io.tio2 = "serial_tx"
scope.io.hs2 = "disabled"
```

### CW305の設定

#### bitstreamの書き込み

- `force=False`だと，FPGAに回路がロードされていない時のみ書き込みが行われる
- `force=True`だと，問答無用で書き込みが行われる

生成済みbitstreamファイルが以下に提供されている．

Artix7 XC7A100Tの場合
```
chipwhisperer/hardware/victims/cw305_artixtarget/fpga/vivado_examples/aes128_verilog/aes128_verilog.runs/impl_100t/cw305_top.bit
```

自分でソースからbitstreamを生成する場合: [公式Tutorial (少し古い)](https://wiki.newae.com/Tutorial_CW305-1_Building_a_Project), [Qiita記事 (公式より新しいが，これも古い)](https://qiita.com/burikama/items/4ce006a8f260fb87e903)


```python
bitstream = os.path.join(
    '..', '..', 
    'hardware', 'victims', 'cw305_artixtarget', 'fpga', 
    'vivado_examples', 'aes128_verilog', 'aes128_verilog.runs', 'impl_100t',
    'cw305_top.bit'
    )

target = cw.target(scope, cw.targets.CW305, bsfile=bitstream, force=True)
```

### プロジェクトファイルの作成

projectオブジェクトのメンバ
- project.location: str
- project.waves: Iterable (M samples x N traces)
- project.textins: Iterable (128 bytes x N traces)
- project.textouts: Iterable (128 bytes x N traces)
- project.keys: (128 bytes x N traces)
- project.get_filename: str 
- project.trace_manager: trace_manager object
- project.save
- project.export


```python
project_file = os.path.join('.', 'projects', 'AES')
project = cw.create_project(project_file, overwrite=True)
```

ジャンパピンの設定

- J16 = 0  
    オンボードPLLからFPGAにクロックを供給

- K16 = 1  
    FPGAのclockをChipWhispererのHS-Inチャネルに接続


```python
target.vccint_set(1.0) # Set VCC-INT to 1V
# we only need PLL1:
target.pll.pll_enable_set(True) # Enable PLL chip
target.pll.pll_outenable_set(False, 0) # Dsable unused PLL0
target.pll.pll_outenable_set(True, 1) # Enable PLL
target.pll.pll_outenable_set(False, 2) # Disable unused PLL2

# run at 10 MHz:
target.pll.pll_outfreq_set(10E6, 1)

# optional, but reduces power trace noise
# 1ms is plenty of idling time
target.clkusbautooff = True
target.clksleeptime = 1 # 1ms typically good for sleep
```

ADCのロックに失敗する場合があるが，何回か試してるうちに成功する． (公式情報)

Liteだとほぼ失敗しないが，Proはよく失敗する


```python
# ensure ADC is locked:
scope.clock.reset_adc()
assert (scope.clock.adc_locked), "ADC failed to lock"
```

## トレースの取得


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

ktp = cw.ktp.Basic()
ktp.setInitialKey('CA FE BA BE DE AD BE EF FE EB DA ED EB AB EF AC') # Secret key

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

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

for i in tnrange(N, desc='Capturing traces'):
    # run aux stuff that should come before trace here

    key, text = ktp.next()  # manual creation of a key, text pair can be substituted here
    textin.append(text)
    keys.append(key)
    
    ret = cw.capture_trace(scope, target, text, key)
    if not ret:
        print("Failed capture")
        continue
        
    # HWでの暗号化の結果が合ってるか確認する
    assert ( list(ret.textout) == list( cipher.encrypt(bytes(text)) ) ), \
        "Incorrect encryption result!\nGot {}\nExp {}\n".format(ret.textout, list(text))
    #trace += scope.getLastTrace()
        
    traces.append(ret.wave)
    project.traces.append(ret)
``` 
    

## Plot


```python
from bokeh.plotting import figure, show
from bokeh.io import output_notebook

output_notebook()
p = figure(plot_width=800)

xrange = range(len(traces[0]))
p.line(xrange, traces[0], line_color="red")
show(p)
```

![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499982/c868842f-6331-12af-de01-af1cae40031d.png)

```python
project.save()
scope.dis()
target.dis()
```

## Attack

- `cwa.leakage_models.last_round_state_diff`

    9->10ラウンド目のSbox_inレジスタのHamming Distance


```python
import chipwhisperer as cw
import chipwhisperer.analyzer as cwa
project_file = os.path.join('.', 'projects', 'AES')
project = cw.open_project(project_file)
attack = cwa.cpa(project, cwa.leakage_models.last_round_state_diff)
cb = cwa.get_jupyter_callback(attack)
```

結果の見方

- 設定した秘密鍵の各バイトが列に対応

- PGE (Partial Guessing Entropy)は正解鍵のランクに対応 (攻撃成功時に0)

- 0~4の行は候補鍵の相関値Top-5を示しており，赤色(正解鍵)がRank0になればそのバイトは攻撃成功となる


```python
attack_results = attack.run(cb)
```


![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499982/c5c16073-1cd9-c38a-9d96-fb5c5e871dd1.png)


### 保存する


```python
import pickle
pickle_file = project_file + ".results.pickle"
pickle.dump(attack_results, open(pickle_file, "wb"))
```

### 答え合わせ


```python
from chipwhisperer.analyzer.attacks.models.aes.key_schedule import key_schedule_rounds
recv_lastroundkey = [kguess[0][0] for kguess in attack_results.find_maximums()]
recv_key = key_schedule_rounds(recv_lastroundkey, 10, 0)
for subkey in recv_key:
    print(hex(subkey))
```

    0xca
    0xfe
    0xba
    0xbe
    0xde
    0xad
    0xbe
    0xef
    0xfe
    0xeb
    0xda
    0xed
    0xeb
    0xab
    0xef
    0xac
    


```python
key = list(key)
assert (key == recv_key), "Failed to recover encryption key\nGot:      {}\nExpected: {}".format(recv_key, key)
```

# 以下，チュートリアルにはない追加部分
自分でCPAしてみる


```python
# 波形のロード
project_file = os.path.join('.', 'projects', 'AES')
project = cw.open_project(project_file)
```

9->10 round目のsbox_inのHDベースのCPA

### 関数定義


```python
from chipwhisperer.analyzer import aes_funcs
from chipwhisperer.analyzer.attacks.models.base import getHW

# HDの計算
# v: previous value of sbox_in register
# k: key candidate
# c: cipher
def hd_model( v, k, c ):
    return getHW( aes_funcs.inv_sbox(v ^ k) ^ c  )

# レジスタのHDの計算
# 順方向: Round9-output -> sbox -> shift_row -> add_round_key
# 求めたいのは HD(Round9-output, cipher)
def calc_hd(cipher, target_byte):
    
    v = cipher[target_byte]
    c = cipher[ np.where(np.array(aes_funcs.inv_shiftrows(list(range(16)))) == target_byte)[0][0] ] # inv_shift_row(cipher)
    
    hd = []
    for k in range(256):
        hd.append([np.array(hd_model(v, k, c), dtype=np.float32)])
    
    return np.concatenate(hd, axis=0)

# ピアソンの相関係数の計算
# x, yの相関を計算する
# yが2次元の場合，axis=1に沿って相関を計算する
def pearsonr(x, y):
    '''
                sum( ( x-x.mean ) * ( y-y.mean ) )
    r = --------------------------------------------------
            root( sum( (x-x.mean)^2 * (y-y.mean)^2 ) )

    '''
    
    x = np.asarray(x, dtype=np.float64)
    y = np.asarray(y, dtype=np.float64)

    # xとyの次元数がともに1のとき
    #普通に相関を計算する
    if x.ndim == 1 and y.ndim == 1:
        mx = x.mean()
        my = y.mean()
        xm = x-mx
        ym = y-my

        r1 = np.sum( xm * ym )
        r2 = np.sqrt( np.sum(xm**2)
                            * np.sum(ym**2) )

        ret = r1/r2
        if np.isnan(ret):
            return 0
        else:
            return ret

    # yの次元数が2の時
    # yの2次元目に沿って相関を計算する
    if x.ndim == 1 and y.ndim == 2:
        assert(x.shape[0] == y.shape[1]), "size of axis 1 is must be same size of x, x:{} != y:{}".format(x.shape[0], y.shape[1])

        xm = x-x.mean()
        ym = y-y.mean(axis=1).reshape(-1,1)

        r1 = np.sum( xm.reshape(1,-1) * ym, axis=1 )
        r2 = np.sqrt(
                np.sum(xm*xm).reshape(-1,1)
                * np.sum(ym*ym, axis=1)
             )

        ret = (r1/r2).reshape(-1)
        ret[np.isnan(ret)] = 0
        return ret
```

### 攻撃


```python
target_byte = 0 # 攻撃対象のバイト鍵
correct_key = project.keys[target_byte] # 正解鍵
outs = project.textouts # Cipher
waves = project.waves # 電力波形
```

各候補鍵におけるレジスタのHDを計算する


```python
hds = []
for o in outs:
    hds.append([calc_hd(o, target_byte)])
hds = np.concatenate(hds, axis=0)
```

レジスタのHDと電力波形の相関を計算する


```python
ret = []
for i in range(256):
    ret.append([pearsonr(hds[:, i], np.array(waves).T)])
ret = np.concatenate(ret, axis=0)
```

結果のプロット


```python
import matplotlib.pyplot as plt
from chipwhisperer.analyzer.attacks.models.aes.key_schedule import key_schedule_rounds

# Mean waveform
fig1 = plt.figure()
ax1 = fig1.add_subplot(111)
ax1.plot(np.mean(waves, axis=0))
ax1.set_title('Mean waveform')

# CPA result
fig2 = plt.figure()
ax2 = fig2.add_subplot(111)
ax2.plot(ret.T, 'gray')
ax2.plot(ret[ aes_funcs.key_schedule_rounds(correct_key, 0, 10)[target_byte], :], 'red')
ax2.set_title('target_byte: {}'.format(target_byte))

plt.show()
```


![output_45_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499982/b43edd79-f0e1-0c7d-94fc-b876ee408e67.png)

![output_45_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499982/8903571b-f30b-a965-537c-d8d9abe5cb2a.png)


10ラウンド目の計算を行っているタイミングで相関が正解鍵の相関が最大になっていることがわかる．

おわり．

## メモ：v5.5.2で発生する接続エラーのバグ探しの途中経過
原因となるコードはだいたいここ[(chipwhisperer/hardware/naeusb/naeusb.py)](https://github.com/newaetech/chipwhisperer/blob/5.5.2/software/chipwhisperer/hardware/naeusb/naeusb.py)．

`NAEUSB_Backend`クラスの定義(L252-)内，有効なデバイス一覧を取得する関数`get_possible_devices`(L422-)において，以下のようなバグが観測された．

- L448 `dev = list(usb.core.find(**my_kwargs))`にて，変数devに接続可能なデバイスオブジェクトのList(CW-LiteとCW305の2台ぶん)が代入される．

- 変数devの要素数が2以上なので，以下のループ(L460-468)が実行される．

```
    for d in dev:
        try:
            d.serial_number
            devlist.append(d)
        except ValueError as e:
            if "langid" in str(e):
                naeusb_logger.info('A device raised the "no langid" error, it is being skipped')
            else:
                raise
```

ここで，正常な場合でも`d.serial_number`の実行時にValueError例外が発生する．
また，発生する例外に`langid`が含まれないためそのまま無名の例外が発生し，そのままに接続エラーとなる．

なぜか，`d.serial_number`をtryの外で一度呼んでやるとCW-Liteとの接続に成功する(が，その後CW305が繋がるかはテストしていない)．
```
    for d in dev:
	d.serial_number
        try:
            # d.serial_number  #  ちなみに，tryの外で一度呼ぶと，tryの中で呼んでも例外は発生しないよう．    
            devlist.append(d)
        except ValueError as e:
            if "langid" in str(e):
                naeusb_logger.info('A device raised the "no langid" error, it is being skipped')
            else:
                raise
```

あとnaeusb.pyを5.6.0のものにそのまま置き換えても動いた．
5.6.0の場合，こんなにややこしいコードにはなっていないのでそもそもこのようなバグには遭遇しそうにない．多分．