Skip to content

Instantly share code, notes, and snippets.

@sylph01
Last active May 24, 2024 15:47
Show Gist options
  • Save sylph01/0a620d726a5bd0abcd19a71813890a92 to your computer and use it in GitHub Desktop.
Save sylph01/0a620d726a5bd0abcd19a71813890a92 to your computer and use it in GitHub Desktop.
RubyKaigi 2024 "Adding Security to Microcontroller Ruby" 日本語解説 / るびまに出すことを想定した原稿

RubyKaigi 2024 "Adding Security to Microcontroller Ruby" 日本語解説

sylph01 / 梶原 龍

本記事は発表のスライドに沿って解説していくスタイルを取っています。スライドは以下のURLにありますのでこちらを開きながら読んでください。

https://speakerdeck.com/sylph01/adding-security-to-microcontroller-ruby

自己紹介 (p.3-5)

梶原 龍(かじわら-りょう)といいます。インターネットではsylph01(しるふ-ぜろいち; だいたい数字の部分が省略して呼ばれがち)と名乗っています。デジタルアイデンティティとセキュリティにフォーカスしたフリーランスのWebプログラマをしております。また、W3CやIETFなどの標準化団体にてインターネット標準の編集や実装に携わっていました/います。

趣味では音楽ゲーム(特にDDR)をしたり、オーケストラでファゴットを吹いたり、鉄道に乗ったり、キーボードを作ったりしています。

発表に至るまでの経緯 (p.7-16)

暗号技術やそれを利用したプロトコルに関心があり、Rubyに足りていないものの実装を行っていました。RubyKaigi 2024に先立って開催されたRubyConf Taiwan 2023にてHybrid Public Key Encryptionの実装の話をしました。ここではRubyのOpenSSL gemを利用する形でのgem実装と、OpenSSL 3.2以降についているHPKEのAPIを利用してRubyのOpenSSL gemそのものを拡張する形での実装の両方を紹介しました。同じく台湾で登壇されていたPicoRubyの開発者の羽角さんと話しているうちにPicoRubyの暗号機能の話になり、PicoRubyに暗号機能を追加することでTLSで通信できるようになるのではないか?ということで、その実装を行うことにしました。本発表はPicoRubyにおける暗号機能と、これまで足りていなかったWiFiを利用したネットワーク機能の実装を扱います。MicroPythonにはあるのでPicoRubyでできないハズはない…!

なおタイトルはASMRとの掛詞なので、実際は「セキュリティの追加」というよりは「ネットワーキングの追加」のほうが正しいと思われますが、スライドにネタを入れてしまう習性なので…。

対象の環境はRaspberry Pi Pico W + PicoRuby/R2P2です。Raspberry Pi Pico WはRP2040というマイコンを搭載しており、これは133MHzで動作するデュアルコアのARM Cortex-M0+を持ち、264KBのSRAMと2MBのフラッシュを持ちます。通常のデスクトップに比べて極めて性能の制限が強いことがわかると思います。Pico WはPicoに加えてCYW43439チップによるBluetoothとワイヤレスLANをサポートするもので、技適認証を通っているので日本でも安心して利用できます。会場付近に「電波法を守ろう」とか「技適マークついてますか?」という広告がたくさんあってギョッとしました。そしてお値段は4月時点で1353円、発表時点でのレートで10ドルを切ります。

注意事項 (p.17-18)

暗号APIは誤って用いると簡単にセキュリティを損ないます。ご家庭で真似する分には大丈夫ですが、Productionで利用する際には専門家のレビューを得てください。

PicoRuby/R2P2のmasterブランチに入っているものはproduction-readyであると思いますが、まだ入っていないネットワーク機能に関してはかなり試験的な荒削りな実装になっています。

また、今回私はPico Wを「普通のコンピュータ」として扱ってプログラミングしているため、組み込み固有のバグみたいなものを踏んでいるかもしれません。マイコンを「普通のコンピュータ」として扱えることは全く自明ではなく、これはPico SDKやPicoRuby/R2P2によるサポートが非常に強力なためできているといっても過言ではありません。

Part 1: PicoRubyにおける暗号 (p.20-37)

21ページから23ページではCRubyにおいてSHA256(ハッシュ関数)やAES(共通鍵暗号)をどのように使えるかを記述しています。CRubyにおいては暗号機能を利用する場合OpenSSL gemを使うのが一般的です。ではOpenSSLを組み込み環境で利用できるかというと必ずしもそのようには行きません。なぜならOpenSSL全体を組み込むとサイズが非常に大きくなってしまうためです。組み込み用の暗号ライブラリ、例えばMbed TLSやwolfSSLなどでは、必要な機能のみを指定してビルドするということができ、最終的なバイナリサイズを小さく抑えることができるようになっています。Pico SDKにおいてはMbed TLSが使われているので、ここではMbed TLSのラッパーを実装していきました。

暗号機能を実装するにあたっては暗号化やハッシュ化の結果であるバイナリ文字列の中身を人間に可読な形で確認したいです。しかしPicoRubyはサイズの制限が厳しいため人間にやさしい機能をいちいち実装しているスペースの余裕がなく、Base16やBase64はもともと用意されていませんでした。そこで最初に取り掛かったのがPicoRubyで動作するBase16とBase64のmrbgemの実装です。これらのgemは単機能の実装で非常にコンパクトな実装になっているため、PicoRubyにおいてC拡張を持つmrbgemを実装したい人にとってわかりやすい入口になると思われます。

実際に実装できたAESとSHA256のPicoRubyでの利用例が26ページと27ページにあります。PicoRubyにはもともとMbed TLSを利用したCMACの実装があり、これをベースに最もよく利用される共通鍵暗号アルゴリズムのAESとその中で最も利用される暗号利用モードのCBCとGCM、最も利用されていて現在安全であると知られているSHA256の実装を追加しました。SHA-1やMD5を足していないのはこれらのアルゴリズムは危殆化したため使ってほしくないからです。

30ページから先はCRubyとmruby/cにおけるC拡張の実装の比較を行っています。Cライブラリのラッパーを書く場合Cの値をRubyオブジェクトに包みそれをインスタンスで引き回すということをよく行います。mruby/cでは mrbc_instance_new() 関数でインスタンスを作成する際に関連づける値のサイズを指定して領域を確保し、インスタンスの data 要素でこの領域を利用します。また、CRubyの rb_define_method で定義するメソッドは指定した関数ポインタで示される関数の引数から直接メソッドの引数を取得できるのに対して、mruby/cの mrbc_define_method で指定する関数ポインタで示される関数はシグネチャが決まっており可変長の引数を扱えるようになっていません。mruby/cでメソッドの引数を取得するには GET_ARG() マクロで取得することになります。

(補足: スライドではCRubyにおけるCの値のラッピングについてかなり大雑把に説明していますが、実際はマクロ1個で済むわけではありません。CRubyにおけるCの値のラッピングの詳細はこの発表の範囲を飛び越えるので、ruby/rubyのextensionに関するドキュメントThe Definitive Guide to Ruby's C APIの第10章を参照してください。)

この情報をもとに、32ページと33ページの例を見てみましょう。

32ページではハッシュ(Digest)のインスタンスを生成する例を示しています。アルゴリズムIDを GET_ARG(1) で取得し、インスタンスの生成を mrbc_instance_new で行っています。ここで確保する領域はMbed TLSにおけるメッセージダイジェストの状態を保持する値の型である mbedtls_md_context_t のサイズです。確保した領域 self.instance->data へのポインタを取得し、それに対して mbedtls_md_init() を呼ぶことで、インスタンスにCの値を関連づけています。

33ページではDigestのインスタンスメソッドであるupdateの実装を示しています。入力文字列を GET_ARG(1) で取得し、 v->instance->data でインスタンスに関連付けられた状態へのポインタを取得し、これらを利用してMbed TLSの関数 mbedtls_md_update() を呼んでいます。また、関数末尾にて mrbc_incref() をインスタンスに対して呼ぶことでこのインスタンスの参照カウントを追加し、関数の終了時にdeallocateされることを回避しています。これを呼ばないとこのメソッドの末尾で該当のインスタンスは寿命を終えたものとしてdeallocateされるため、次にインスタンスメソッドを呼んだ際にはsegmentation faultとなってしまいます。

今回PicoRubyの暗号機能を実装するにあたって、複数回 update を呼ぶ形のAPIしか実装しておらず、バッファをひとまとめに暗号化するAPI(one-shot API)を実装していません。これはone-shot APIは暗号化対象のバッファと同じサイズの書き込み先バッファが必要であるため、メモリが小さい環境では小さいバッファに対して処理を繰り返し処理が終わった領域を解放していくことができる形のAPIのほうが有利であると考えられるからです。

また、多くの暗号処理においては乱数が必要ですが、パソコンと違って乱数生成器が事前に用意されておらず、ハードウェアの機能を使って自分で実装する必要があります。Raspberry Pi PicoにはRing OscillatorというNOTゲートを輪っかのようにつないだものがあり、呼び出すタイミングによって0と1のどちらかが得られる、というものがあります。これをCで利用できるようにしたのが37ページの実装です。ここでは単にハードウェアから取り出したビット rosc_hw->randombit をそのまま利用しているのではなく、01を0に、10を1に、00と11を捨てるという処理を行う(von Neumann debiasing)ことで、ハードウェアから取り出したビットの偏りを低減させる処理を行っています。

(補足: 0が得られる確率がpとしたとき、1が得られる確率は(1-p)であるため、01が得られる確率はp(1-p)、10が得られる確率も同じくp(1-p)、00と11は捨てるので、結果として01と10をそれぞれ0と1にした場合元の値を利用するよりも偏りが低減していることになります)

Part 2: ネットワーク (p.39-60)

ネットワーク機能の実装とは、具体的に以下のことを目指します:

  • 802.11を利用して無線LAN接続を行う(レイヤー2)
  • IPアドレスを取得する(レイヤー3)
  • サーバーに対してTCPで接続する(レイヤー4)
  • 可能であればTLSで暗号化する(レイヤー5)
  • アプリケーション層のプロトコルとしてHTTPを利用する(レイヤー6/7)

それぞれのレイヤーに対して、Pico SDKの対応するものが以下のように存在します:

  • CYW43439のドライバ: レイヤー2
  • lwIP: レイヤー3・4
  • Mbed TLS: レイヤー5

また、CYW43439ドライバとlwIPの連携、lwIPとMbed TLSの連携がPico SDKで用意されています。HTTPは自分で実装することになります。よって、今回実装したのは、CYW43439ドライバのWiFi関連機能のRubyインターフェース、lwIPのRubyインターフェースと簡易なHTTPライブラリです。Mbed TLSの機能はlwIPから透過的に利用されるので、実はPart 1で実装した暗号機能はTLS接続には使われていません。

R2P2からPico SDKに用意されているネットワーキングのライブラリを追加するCMakeLists.txtの記述が43ページにあります。CYW43439ドライバには pico_cyw43_arch_lwip_pollpico_cyw43_arch_lwip_threadsafe_background の2つのモードがあり、今回は後者を利用しています。前者はメインのプログラムから定期的にドライバをポーリングする必要があるのに対して、後者はその処理をバックグラウンドでやってくれます。リアルタイムOS(RTOS)を利用するモードもあるのですが、WiFiのためだけにRTOSを導入するのはオーバーキルなのでここでは利用していません。

CYW43439ドライバの機能を使ってWiFiアクセスポイントに接続するためのRubyコードが46行目に記載されています。

WiFiアクセスポイントにつながるようになったあと、まずlwIPのDNS機能を利用した名前解決のラッパーを実装しました。lwIPの多くの関数は、ネットワーク処理を実行してデータが準備できた際に呼び出されるコールバックを関数ポインタで設定する形で動作します。48ページのコードがその実例を示しています。lwIPの dns_gethostbyname() 関数は第3引数にコールバック関数を設定します。名前解決が終了しIPアドレスが得られたとき、dns_found コールバックが呼ばれ、その第3引数 void *arg に結果のIPアドレスが格納されるので、コールバック関数内で結果を格納したい領域のポインタに対して名前解決の結果をコピーしています。

ところでデモ動画ではHTTPS接続の1回目が失敗していました。これは何ででしょう?というわけでRaspberry PiをWiFiホットスポットとして立ち上げ、その間の通信をWiresharkで覗いてみることにしました。設定の方法は公式のチュートリアルに記載されています。実際のパケットキャプチャの結果が50ページに記載されています。失敗した例では 3.0.0.0 というIPアドレスに対してTCP接続を試みて失敗していることがわかります。これはDNSの解決結果を格納するIPアドレスの領域が適切に初期化されていないために名前解決の結果を待たずに意図しないIPアドレスに対してTCP接続をしようとしていた結果でした。適切な初期化を行う修正をしたところDNSの名前解決を待ってそのIPアドレスに対してリクエストを行っていることがわかります。

続いてTCPクライアントの実装です。スライドでは紙面の関係でかなり端折った説明になっていますがここでは実際のコードとセットで見ていきましょう。

  • Rubyコードに対するインターフェースがTCPClient_send()です。
    • 223行目の ip4_addr_set_zero() が上記の「適切な初期化を行う修正」に相当します。
    • 230行目の TCPClient_connect_impl() にてTCP接続を初期化し接続を行います。
      • TCPClient_new_connection() がTCP接続を作成している部分です。altcp_new() でProtocol Control Block(PCB)を作成し、続く altcp_recv(), altcp_sent(), altcp_err(), altcp_poll() で状態に応じて呼ばれるべきコールバックを設定しています。コールバック関数に渡される引数として接続状態を示す値を渡してほしいので altcp_arg() にて設定しています。ここで特に重要なのがデータを受け取ったときに呼ばれるコールバックを設定する altcp_recv() です。
    • TCPClient_send 内のwhileループで呼んでいる TCPClient_poll_impl() はTCP接続の状態に応じて行うべき処理が記述されています。接続が完了した状態 NET_TCP_STATE_CONNECTED になったとき、 altcp_write() を利用して send_data というmruby/c文字列の中身をネットワークに書き出し、 altcp_output() で書き出しの完了を待ちます。
    • altcp_recv() で設定した TCPClient_recv_cb()の中身を見ていきましょう。受信したデータは pbuf と呼ばれる構造体に入って渡ってきます。この pbuf はlinked listのデータ構造を持っているので、82行目〜86行目でこのリストを順番にたどることでデータを一時バッファにコピーしています。この一時バッファの中身を mrbc_string_append_cbuf() を使って受信データを示すmruby/cの String に対して結合してあげることで受信したデータの String を生成しています。
    • 空の pbuf を受信したり接続がidleになった場合 NET_TCP_STATE_FINISHED という状態に移行します。 TCPClient_poll_impl() の中でPCBを閉じたり接続状態を管理する変数を解放したりしています。この結果whileループを脱し、 recv_data オブジェクトが準備できるので、それを返り値として返却しています。

TCPクライアントができればHTTPはそんなに難しくありません。Rubyの文字列でHTTPリクエストを組み立ててあげればよく、それをTCPClientに対して渡せばよいです。最も原始的なHTTP GETのクライアントを55ページに記載しています。執筆時点での実装ではこれに加えて、得られたレスポンスからステータスコードとヘッダとボディを分割するところまで実現しています。

TLSはlwIPのApplication Layered TCPの機能を使えば少々の書き換えで実現できます。TCPClient_new_tls_connection()TCPClient_new_connection() に対する差分を説明すると、

  • altcp_tcp_create_client_config() で設定を用意し
  • altcp_new() の代わりに altcp_tls_new() でPCBを作成し
  • mbedtls_ssl_set_hostname でホスト名を設定する

だけの差分しかありません。

何が起こっているのかというと、TLSのPCBが渡されたとき、lwIPはデータ送信時にはTCPの送信用関数が呼び出されたあとにTLSのコールバックを呼ぶことで暗号化を行い、逆に受信時にはTLSのコールバックが先に呼ばれて復号されてから平文がTCPのコールバックに対して渡されることで、透過的に暗号処理が行われるため、平文の場合とTLSの場合でコードを共用することができています。

スライドには「TLSは比較的自明に実装できる」と書いていたのですが、実はTLSを追加したときに突然謎のハングアップに見舞われたため、そんなに自明ではありませんでした。これはlwIPのメモリ管理とmruby/c VMのメモリ管理が独立に動いており、TLSを追加したことでメモリが足りなくなったためでした。そのためPicoRuby側のヒープメモリサイズを元の194KBから96KBと半分近く削っています。また、別のところでは mrbc_free() でメモリ解放するべきところを、存在しない free() で解放しようとして謎のハングアップで3時間ほど失ったこともありました。

Part 3: 今後の開発について (p.62-65)

今回はTCPクライアントをlwIPの機能を使って愚直に実装しましたが、CRubyでは TCPSocketSocket.tcp によってBSD Socket APIに沿った形のAPIが提供されています。MicroPythonでも同様のソケットAPIが存在しています。MicroPythonも同様にPico SDKのlwIPを利用しているため、これをポーティングすることでPicoRubyでもソケットのようなAPIでネットワーキングが実現できるかもしれません。

また、クライアントを実装したということはサーバー機能も欲しくなります。ソケットAPIがあれば比較的やりやすいでしょう。実はlwIPの機能をそのまま使う形で実装を進めているのですが、mruby/cのほうに足りないと思われる機能がありRubyで実現したいAPIデザインが実現できずに保留しています。

今回実装したネットワーク機能はブロッキングIOを利用していますが、ノンブロッキングIOが必要かといわれると現時点では確証を持てていません。

まとめ (p.67-79)

ここまでの実装を通して、PicoRubyを使ってHTTPSのリクエストを送る機能が実現できました。発表時点ではBase16/64、SHA256、AESと乱数生成がPicoRuby/R2P2のmasterブランチに入っており、ネットワーク機能は今後いくつかの修正を経てpull requestを出す予定です。

しかしPicoRubyでTLSが本当に必要かと言われるとまた別の問題があります。似たような環境のベンチマーク(STM32L562E, Cortex-M33 @ 110MHz on wolfSSL)では2048bitのRSAは1秒に0.155回しか実行できない、つまり6秒前後かかる、という結果が得られているように、公開鍵暗号は組み込み機器で実行するには非常に高コストな演算です。また、TLSのルート証明書をインストールしていないため、TLSの暗号化部分を得ることはできますが、接続先が本当に意図した接続先であるかの認証を得ることはできません。そのため、セキュリティ要件によってはゲートウェイとの間をアプリケーションレベルの共通鍵暗号で通信し、ゲートウェイからTLS接続する、というような構成も考えられるでしょう。この場合共通鍵はPicoの上に存在しているためPicoが物理的に侵害された場合は共通鍵が奪われてしまいますが、ネットワークに接続するためのWiFiパスワードもPicoの上に存在しているので、物理的侵害を本気で気にするならばもっと別の方法を使う必要があるでしょう。

もっと普通のコンピュータに近いプログラミング体験が欲しいならばRaspberry Pi Zero 2 Wというもう少し強力なハードウェアを使うのも手でしょう。これはLinuxを実行できSSHすることもできるので、なんならPicoRubyすら使う必要もありません。Picoを使うのは組み込みやすいからです。実際のIoT環境ではこれらの違ったスペックを持つハードウェアは協調して動作させるので、PicoRubyがWiFiに繋がるようになったことで、「Rubyでの真のIoT」は現実に近づいたといってよいでしょう。IoTに関するプロトコルは数多く存在しており、これらをPicoRubyで扱えるようにすることで、RubyでのIoTの可能性は広がっていくものと考えられます。このことから、RubyでのIoTは「ブルーオーシャン」であるといえます。コミュニティは皆さんのコントリビュートをお待ちしています。

謝辞 (p.80-81)

まずはPicoRuby/R2P2の作者である羽角さん(Twitter: @hasumikin)に最大限の感謝をしたいと思います。このプロジェクト自体PicoRuby/R2P2なしには全く成立し得ないものです。また、実装の過程で多くのアドバイスをいただきました。

また、本発表はRubyKaigiでネットワークに関するトークを行った先駆者であるうなすけさん(Twitter: @yu_suke1994)としおいさん(Twitter: @coe401_)に大きなインスピレーションを受けています。今後もRubyにおけるネットワークプロトコル実装をやっていきましょう!(読者向け: ruby-jp Discordの #ietf チャンネルにてIETF Meetingの前に最新のInternet-Draftを読んでいく会をやっていますので興味のある方はぜひご参加ください。IETF自体膨大なワークを扱っているのでRubyでカバーできる範囲が増えることはコミュニティのためにもなります!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment