STM32のRTCのタンパ検出


Tamper(=改ざん)を検出・防止するための仕組みがSTM32には備わっている。
RTCモジュールの機能として組み込まれ、ピン(PC13等)にエッジが入ったりレベルが変化すると改ざんが試みられたと判断し、一部のメモリの内容を削除する。また、それが行われた日付を記録する機能もある。

今回は押しボタンでタンパピンを制御しているが、本来はケースの中に這わせた導線が切断されたりしたことを検出して筐体が開封されたと判断し、機微な情報(秘密鍵等)を削除するために使う。

タンパの初期化

RTC_TamperTypeDef tamper = {
    .Tamper = RTC_TAMPER_1,
    .PinSelection = RTC_TAMPERPIN_DEFAULT,
    .Trigger = RTC_TAMPERTRIGGER_LOWLEVEL,
    .Filter = RTC_TAMPERFILTER_2SAMPLE,
    .SamplingFrequency = RTC_TAMPERSAMPLINGFREQ_RTCCLK_DIV512,
    .PrechargeDuration = RTC_TAMPERPRECHARGEDURATION_8RTCCLK,
    .TamperPullUp = RTC_TAMPER_PULLUP_ENABLE,
    .TimeStampOnTamperDetection = RTC_TIMESTAMPONTAMPERDETECTION_ENABLE,
};
HAL_RTCEx_SetTamper(&hrtc, &tamper);
__HAL_RTC_TAMPER1_ENABLE(&hrtc);

GPIOの初期化は不要(マイコンリセット時等、GPIOの管轄外のタイミングでも動作する必要があるため、独自に管理されている)。

上記設定の場合、RTC_TAMPER_1にPC13が使われ、ピンが2回ローレベルになるとタンパと判断し、検出の間隔は512RTCクロックで、検出までのプリチャージが8RTCクロック、プルアップが有効で、タンパを検出したらタイムスタンプレジスタに日付をコピーする、というような動作。

RTCクロックは、例えばLSEの32.768kHzが使われる。512分周の場合は64Hz(15.625msec)周期で行われ、プリチャージが8クロックの場合はプルアップを有効にしてから約244usec後にサンプリングされ、その後プルアップが開放される。


黄色がVbat、紫色がPC13。VDDで動作中。
64Hzでプルアップが有効になり、244usの遅延の後にサンプリングされ、プルアップが解除されている。今回はPC13の容量が小さいので遅延の効果は無いが、ある程度の負荷容量がある場合は立ち上がりが遅くなるから、適切な遅延の設定が必要となる。


タンパが検出されるとRTC.ISR.TAMP1Fがセットされ、RTC.BKPxRの内容がクリアされる。

再びタンパ検出を行うにはTAMP1Fをクリアする必要がある。
例えば

static constexpr uint32_t calc_BB(uint32_t peripheral,
                                    uint32_t register_offset,
                                    uint32_t address_number)
{
    return (PERIPH_BB_BASE +
            ((peripheral - PERIPH_BASE) + register_offset) * 32 +
            address_number * 4);
}

のような関数を定義しておけば、ペリフェラルの特定ビットをビットバンドでアトミックに操作できるので、

constexpr uint32_t ISR_TAMP1F_BB = calc_BB(RTC_BASE, 0x0C, RTC_ISR_TAMP1F_Pos);
*reinterpret_cast<volatile uint32_t *>(ISR_TAMP1F_BB) = 0;

というふうにすれば、一挙動でTAMP1Fをクリアできる(0x0CはISRレジスタの位置)。


タンパが発生した場合はタイムスタンプが記録されるので、発生した日時を確認できる。
例えば以下のような感じ。

RTC_TimeTypeDef time = {};
RTC_DateTypeDef date = {};
HAL_RTCEx_GetTimeStamp(&hrtc, &time, &date, RTC_FORMAT_BIN);

printf("%04lu/%02hhu/%02hhu %02hhu:%02hhu:%02hhu\n",
        date.Year + 2000UL, date.Month, date.Date,
        time.Hours, time.Minutes, time.Seconds);

タイムスタンプが記録されるのは最初のタンパのみで、以降にタンパが発生しても記録されない。


Aの位置でVDDを切断している。それ以前はPC13はVDDで駆動されていたから、Vbatよりも高い電圧で出ている(Vbatはダイオード経由でVDDに接続)。
Bの位置でバックアップキャパシタを少し放電している。
Cの位置でPC13をGNDに落としている。プルアップが地絡するのでパルスが出なくなる。
Dの位置でも放電を行っている。
Eの位置でVDDを復旧させた。PC13のプルアップは直ちにVDDで駆動され、Vbatは抵抗経由で充電されている。

このように、「ローレベルで改ざんと判定」では、オシロで見るとタンパ検出を行っていることが簡単に把握されてしまう(もっとも、プローブを突っ込まれている時点で(筐体が開封されている時点で)タンパ検出を突破されているわけだが)。
今回はノーマリーオープンのモーメンタリスイッチでPC13を制御しているのでLowでタンパと判定しているが、本来はノーマリークローズの回路で地絡しておき、Highでタンパと判定したほうがいいはず。また、プルアップが地絡して判断の場合は「スイッチが正常に動作することによって」判定しているが、プルアップでHighを判定する場合は「回路が開放(破壊)されたら」判定なので、フェイルセーフ的にも正しいと言える(回路閉で判定する場合、回路を切断されたら判定できなくなる)。

常に地絡させておく場合、プルアップの電源はバックアップバッテリから供給されるので、容量が小さいと短期間で放電してしまう可能性がある。プリチャージ時間を短く設定したり、十分に容量の大きな電池を使ったり、といった工夫が必要。
逆に、小さい容量のキャパシタ(数十uFオーダー)を使うことで、一定期間停電したら機微な情報は破棄する、みたいな使い方もできる(CR特性に依存するので信頼性は悪いが)。暗号化機能がついたコアを使えば、PCから抜いて数分放置されたらデータが破壊されるUSBメモリ、みたいな使い方もできる(モバイルチャージャ持ち込まれたら意味ないけど)。

タンパを検出しても、以降のタンパ検出パルスが継続されるので、攻撃者が同じデバイスを複数個用意し、いくつかは破棄しても良い場合は、どれかを開封してピンを見れば、パルス周期やパルス幅が把握されてしまう。
エッジ検出であればそのような危険は無いが、プルアップ経由でVbatが常に放電されてしまう。


タンパが検出された場合、RTCのバックアップレジスタ20ワードがクリアされる。4KiBのバックアップSRAMは無関係であることに注意。タンパが検出された場合、フラグをクリアしないとバックアップレジスタへの書き込みはできなくなる。すでに筐体が開封されている以上、無闇矢鱈とタンパフラグを解除するのもどうかと思うので、実際に使う場合はフラグの確認とかせずにガンガン書き込んでいくべきかもしれないけど。

多機能な上位のコアであれば暗号化モジュールが内蔵されているからAES256等を使えるけど、今回使用したコアでは暗号化は使えないので、ソフトウェアで実装するか、暗号は扱わないか、というような使い方になる。


おまじない
機微な情報を扱う場合はしっかりと評価を行い、必要であればメーカーにサポートを求め、楽観的な判断(「多分動いているだろう」等)は行わないこと。というか漏れちゃまずい情報を扱う人が、素人が書いたQiita記事なんぞ参考にしないでくれ。頼むから。
この記事を参考にするなら、せいぜいアミューズメント施設の謎解きの暗号装置に使って「腕に自身のある人は解析してみろや!」くらいの、突破されても問題ないような用途に限って使うこと。