aimdevel’s blog

勉強したことを書きます

github actionsでyoctoの脆弱性情報をチェックする

始めに

前回はyoctoでビルドしたOSの脆弱性情報をファイルで取得するところまで作成した。

aimdevel.hatenablog.com

今回はその結果に未対応の脆弱性があるかどうかを判定できるようにしていく。

実施内容

脆弱性情報は前回の記事で取得できているので、その内容をチェックする以下のスクリプトを作った。

脆弱性対応状態チェックスクリプト

meta-my-raspberrypi/ci/cve-check/check_yocto_cve_json.py at main · aimdevel/meta-my-raspberrypi · GitHub

このスクリプトは以下のように使用できる。

$ ./check_yocto_cve_json.py \
        ./core-image-base-raspberrypi4-64.json \
        --config ./config_sample.json

core-image-base-raspberrypi4-64.jsonは、取得したcve情報の入っているjsonファイル。
config_sample.jsonは、ツールの設定ファイルで、今はチェック対象から除外するパッケージを書けるようになっている。一時的に除外しないとgithub actionsのワークフローがいつまでたっても成功しないということになりかねないので。正式に対応する場合はyoctoの作法に従って脆弱性対応するべき。

このスクリプトが出力する未対応の脆弱性とは、cve情報のjson内でstatusがUnpatchedになっているものである。例えば以下のようなもの。

        {
          "id": "CVE-2010-4756",
          "summary": "The glob implementation in the GNU C Library (aka glibc or libc6) allows remote authenticated users to cause a denial of service (CPU and memory consumption) via crafted glob expressions that do not match any pathnames, as demonstrated by glob expressions in STAT commands to an FTP daemon, a different vulnerability than CVE-2010-2632.",
          "scorev2": "4.0",
          "scorev3": "0.0",
          "scorev4": "0.0",
          "vector": "NETWORK",
          "vectorString": "AV:N/AC:L/Au:S/C:N/I:N/A:P",
          "status": "Unpatched",
          "link": "https://nvd.nist.gov/vuln/detail/CVE-2010-4756"
        },

github actionsのワークフローに組み込み

上記のスクリプトをワークフロー内で実行されるようにする。

meta-my-raspberrypi/.github/workflows/cve_check.yml at main · aimdevel/meta-my-raspberrypi · GitHub

実行個所は以下。特に何のひねりもなく実行しているだけ。

      - name: Check CVE
        run: |
          ./ci/cve-check/check_yocto_cve_json.py ./build/tmp/deploy/images/raspberrypi4-64/core-image-base-raspberrypi4-64.json --config ./ci/cve-check/config_sample.json

スクリプトは未対応の脆弱性が存在するとexit(1)で終了するので、その時はワークフロー自体も失敗で終わる。
ワークフローが失敗すると実行者にメールで通知が飛ぶので、脆弱性がある場合はそのメールで気づけるはず。ほんとはslackとかに通知を送れたほうがかっこいいが、今回は面倒なのでやらない。

ついでに

yoctoビルドに時間がかかりすぎているので、それを改善するべくキャッシュをローカルに保存するように設定を追加した。

      - name: set shared yocto cache path
        run: |
          echo "DL_DIR = \"/opt/yocto-cache/downloads\""   >> build/conf/local.conf
          echo "SSTATE_DIR = \"/opt/yocto-cache/sstate-cache\""  >> build/conf/local.conf

完全に個々のrunnerに保存してしまっているので、新規ランナーでは通常通りのビルド時間がかかるが、2回目以降は高速に実行できるようになると思う。

終わりに

  • yoctoビルドで得られた脆弱性情報のファイルをパースして未対応の脆弱性があるかを判定できるスクリプトを作成した。
  • 作成したスクリプトgithub actionsで実行して未対応の脆弱性がある場合にはワークフローが失敗するようにした。

これでワークフロー定期的に実行するように設定しておけば、脆弱性がある場合にワークフローが失敗するので、素早く脆弱性に気づくことができるはずだ。

yoctoで作成したOSの脆弱性情報をgithub actionsで取得する

yoctoでビルドしたOSに含まれる脆弱性情報の取得をgithub actionsで行ってみた。

初めに

過去の記事でgithubのDependbot alertに対応したときに、依存の中に脆弱性が含まれている際に通知してくれる仕組みの便利さを味わった。

aimdevel.hatenablog.com

cなどのパッケージマネージャがない言語では、このような仕組みは難しいと思っているが、yoctoでビルドしたイメージの中に脆弱性があるかどうかくらいは通知できるのではないかと思いついた。
実際に過去の記事でcve-checkを使って脆弱性のチェックを行っているし、github actionsと組み合わせれば自動通知の仕組みを作るのはそこまで難しくない気がするし。
いきなり通知するところまで作るのはハードルが高いので、本記事ではyoctoビルド時に得られるcve情報をgithub actionsで取得するところまでを行う。
cve-checkに関しては過去に動作確認を行っている。

aimdevel.hatenablog.com

実施内容

  • self-hosted runnerの作成
  • yoctoビルドのワークフロー作成

それぞれ説明する。

self-hosted runnerの作成

yoctoビルドをgithub actionsで実行したいが、githubがホストしているランナーはあまりスペックが高くない。
yoctoビルドを行うには全く足りていないので、自前のself-hostedなランナーを用意する必要がある。

そこで今回は、yoctoビルド用のself-hostedランナーとして動くdockerコンテナを作成した。
作成に使った資材は以下である。

meta-my-raspberrypi/ci/self-hosted-runner at main · aimdevel/meta-my-raspberrypi · GitHub

使い方は以下。

  • イメージをビルドする。
$ docker build -t yocto-build-runner .
  • sample.envをベースに設定を書く。
    例えば、以下のように編集する。
GITHUB_URL=https://github.com/aimdevel/meta-my-raspberrypi
RUNNER_TOKEN=YOUR_TOKEN
RUNNER_NAME=my-yocto-build-runner
RUNNER_LABELS=self-hosted,linux,yocto-build

RUNNER_TOKENに設定する値を取得するには、runnerを使用するリポジトリで、New self-hosted runnerボタンをクリックする。

そしてプラットフォームなどを選ぶ、とランナーを追加するコマンドが表示されるので、tokenだけ抜きだして使えばよい。
例えば、tokenは以下のように表示されているはずである。

$ ./config.sh --url https://github.com/aimdevel/meta-my-raspberrypi --token <your token>
  • コンテナを開始する 以下を実行して少し待つとランナーがgithubに登録される。
$ docker run -it -d --env-file <your env file> yocto-build-runner

yoctoビルドのワークフロー作成

以下のファイルを作成した。

meta-my-raspberrypi/.github/workflows/cve_check.yml at main · aimdevel/meta-my-raspberrypi · GitHub

以下を行うワークフローになっている。

  • cve-checkを有効化

  • yoctoのcore-image-baseをビルド

  • cve情報を取り出して保存

それぞれ説明する。

cve-checkを有効化

      - name: Add cve-check
        run: |
          echo "INHERIT += \"cve-check\""   >> build/conf/local.conf
          echo "BB_NUMBER_THREADS = '6'"  >> build/conf/local.conf
          echo "PARALLEL_MAKE = '-j 4'"   >> build/conf/local.conf

INHERIT += "cve-check"がcve-checkを有効にする設定。
他の2つはビルド時の並列実行の設定で、今回作ったself-hostedランナーのスペックがそこまでよくないため指定してある。マシンスペックに自信がある場合はこの設定は外したほうがよい。

yoctoのcore-image-baseをビルド

通常通りにビルドする。

      - name: Run cve-check
        run: |
          . layers/poky/oe-init-build-env build/
          bitbake core-image-base

cve情報を取り出して保存

以下の2種類のcve情報を取り出す。

  • 個々のパッケージのcve

  • OSイメージ全体のcve

      - name: Upload cve
        uses: actions/upload-artifact@v4
        with:
          name: cve-result
          path: |
            build/tmp/deploy/cve
            build/tmp/deploy/images/raspberrypi4-64/*.rootfs.json
            build/tmp/deploy/images/raspberrypi4-64/*.rootfs.cve

build/tmp/deploy/cveにはパッケージごとのcve情報が格納されている。
build/tmp/deploy/images/raspberrypi4-64/*.rootfs.jsonbuild/tmp/deploy/images/raspberrypi4-64/*.rootfs.cveには、OSイメージ全体のパッケージ情報が記載されている。
.cveファイルと.jsonファイルがあるが、内容的には同じものだと思う。(未確認)

結果確認

github actionsの実行が成功すると、結果画面からartifactをダウンロードできる。
解凍して中身を確認すると、cve情報を取得できていることがわかる。

終わりに

github actionsを使ってyoctoのcve-checkを有効にしたビルドを行い、その結果を取得できるようにした。
今回の実装では手動で実行して結果のファイルを取得するようになっているが、本来は脆弱性監視のために定期実行と通知の仕組みを入れておくべきだと思っている。今回そうなっていないのは、self-hostedランナーを常時稼働させる環境がないのと、区切りのいいところまでで一度記事にしてしまいたかったという理由。
なので、今後は以下をできるようにしたいと思う。

  • cve情報をチェックして通知を飛ばす

self-hostedランナーの常時稼働+cve-checkの定期実行は、安く使える環境があればやってみたい。自宅のPCでやるのはちょっと厳しい。

Devcontainerを活用したYocto Projectのビルド/開発環境構築

初めに

最近業務でdevcontainerを使う機会があったのだが、かなり便利だと感じたので自分用に1つ作ってみた。

作ったもの

Yocto Projecのビルド/開発に使用できるdevcontainerを作成した。

github.com

備えている機能は以下。

  • Yocto Projectをビルド可能
    公式ドキュメントに従ってパッケージをインストールしたコンテナを作成した。
  • vscodeのbitbake拡張機能
    devcontainerの設定で、vscodeのbitbake拡張機能をインストールするようにした。

使い方

vscodeでこのdevcontainerを使う方法を説明する。

初めはシェルで操作を行う

このリポジトリをクローンする。

git clone https://github.com/aimdevel/yocto-devcontainer.git

vscodeでこのリポジトリのルートを開く。

code yocto-devcontainer/

ここからはvscodeの操作を行う。

コマンドパレットからdevcontainerを起動する。
Ctrl + Shift + p でコマンドパレットを開く。
Dev Containers: Reopen in Container を選択する。

devcontainerがビルドされ、vscodeがdevcontainerに接続される。

クローンしてきたyocto-devcontainerのディレクトリがコンテナにマウントされており、ホストとファイルを共有したままYoctoのビルドを行うことができる。

終わりに

Yocto Project用のdevcontainerを作成した。
ほぼビルドできるだけのdevcontainerだが、手っ取り早く環境を立ち上げられるので便利になったとは思う。
このdevcontainerに不足があればまた更新しようと思う。

vscodeの拡張機能Clineを使って自作ツールに機能追加したらすごかった

初めに

AIにツールを改良させたらすごかったので、紹介しようと思う。

今回改良させたツールは以下。

github.com

以前作成した、ダンプしたsdカードイメージの中にあるパーティションをマウントするためのツールだ。 今回はこれに機能を追加させることに挑戦した。

やったこと

vscode拡張機能Clineを使って、追加機能のすべてと、さらにテストも実装してもらった。
使ったモデルはOpen AIのo3-mini。ついこの間一般人でも使えるようになったやつ。
Clineの導入やOpenAI APIの契約方法については、世の中に腐るほど記事があるので割愛。

作った(作らせた)機能

ツールにファイルシステムをフォーマットする機能を追加してもらった。
ファイルシステムのないパーティションをマウントする前にフォーマットできるとよいかなと思って追加した。

Cline君の働きぶり

Cline君にいろいろと依頼したので、その反応を紹介する。

日本語で依頼した場合

以下の内容で依頼してみた。

  • このプロジェクトはファイルにダンプされたsdカードを操作するツールの開発プロジェクトです。 今はすでに存在するパーティションをマウントする機能を実装してあります。 このツールにパーティションをフォーマットする機能を追加してくっださい

  • Cline君

    以下のコードをsrc/main.rsに反映してください: 以下ソースコードの羅列

いや直接編集してくれないのかい。
さすがにファイルの編集は頼めばやってくれたが、追加機能以外の箇所もほとんど変更されてしまったので、一度これはなかったことにした。

改めて英語で依頼

上記の依頼を翻訳して依頼してみた。ちなみに翻訳にもAI(chatgpt)を使った。

  • This project is a development project for a tool that manipulates SD card dumps stored in files. Currently, it has the functionality to mount existing partitions. Please add a feature to format partitions to this tool.

  • Cline君

    I'll first inspect the "src/main.rs" file to understand its current structure, then add a new command-line option (or similar interface) that implements formatting of partitions. I'll check the file now.

このように返事をして早速コードを編集してくれた。また、返事が返ってくるのも日本語の時より早い気がする。
中身を見ると見事にformatサブコマンドが実装されていた。
実装の内容は、指定したパーティションを0x00で埋めるという処理だった。mkfsを実行するような処理を期待していたが、これは自分の指示が悪かったなと思い、さらに追加でvfatとext4にフォーマットできるように依頼した。
その結果求めていた機能が実装された。

テストの実装もさせてみる

雑にテストの実装も頼んでみたところ、失敗するテストが出来上がった。
ログを読んでみると、テストのために作成する一時ファイルが見つからないというエラーだった。
これの原因は、すべてのテストで同じ名前のファイルを使用しているためであった。rustのテストはデフォルトで並列実行されるので、すべてのテストで同じファイルを参照、編集してしまっていた。
これはCline君に頼むまでもないので、自分で直したが。
指示がよくなかったという原因もあるとは思うが、もしかすると並列実行などはそこまで得意じゃないのかな?

作業を終えて

ここまでに試行錯誤しながらCline君に作業を依頼したが、すべて合わせても時間にして30分程度にしかなっていない。
さらに、今回使用したOpenAIのo3-miniの利用料金だが、約0.3ドルで、今の日本円で約45円程度しか使ってない。
実装速度も速いし、値段もそこまで高くないので今回のようなちょっとした作業には向いていそうだ。

気付き

今回、Cline + OpenAI o3-miniで開発して以下のような気づきがあった。

  • 日本語は苦手そう。依頼は英語で行うべき。
  • 融通は利かない。依頼したとおりに実装する。なので、想定したものと異なる結果が出た場合は追加で依頼をしなければならない。
  • 思ったより値段は高くない。まあ、使うモデルによるんだろうけど。

こうして書いてみると、どこかにいるエンジニアに作業依頼する話のように見えてくる。

終わりに

今回はCline + OpenAI o3-miniを使ってツールに機能を追加した。
思ったよりしっかり動くものが出来上がって驚いた。
このようにAIを使っていけばあらゆる開発が加速していくのだろうなと感じた。

Linuxセキュリティ機能を盛り込んだラズパイのアップデート手順:解説編

前回の記事で行ったラズパイのアップデートについて、内容を説明する。

aimdevel.hatenablog.com

対象の資材

以下のリポジトリのmeta-my-securityディレクトリが対象。
https://github.com/aimdevel/meta-my-raspberrypi

各設定の説明

Linuxのアップデートを行うために作成した箇所について説明する。

partitionの設定

sdカードのパーティション設定は、meta-my-security/wic/my-security-dual.wks.in に記載してある。
meta-raspberrypiのwksファイルをベースに作成した。
以下のパーティション構成と同じになるように設定してある。

name size format description
mmcblk0p1 128MB vfat ブートパーティション。u-bootを格納
mmcblk0p2 128MB vfat linuxを格納。アップデートA面
mmcblk0p3 128MB vfat linuxを格納。アップデートB面
mmcblk0p4 none none 拡張パーティション
mmcblk0p5 512MB none rootfs。この領域をdm-verityで検証する。アップデートA面
mmcblk0p6 512MB none rootfs。この領域をdm-verityで検証する。アップデートB面
mmcblk0p7 500MB ext4 書き込み用暗号化領域。初回起動時にdm-cryptで暗号化する。アップデート後もデータが保持される領域として使用する。
mmcblk0p8 20MB ext4 dm-crypt用ファイル置場。キーファイルや初回起動フラグなど。
part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 4096 --size 20
part / --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 4096 --size 20
part / --source rawcopy --ondisk mmcblk0 --sourceparams="file=${IMGDEPLOYDIR}/${DM_VERITY_IMAGE}-${MACHINE}.${DM_VERITY_IMAGE_TYPE}.verity"
part / --ondisk mmcblk0 --size 100M
part / --ondisk mmcblk0 --size 500M
part / --ondisk mmcblk0 --fstype=ext4 --size 20M

このファイルの書き方は以下のページを参考にした。
https://docs.yoctoproject.org/ref-manual/kickstart.html

また、この設定を使用するためにlocal.confに以下の設定を追加。

WKS_FILE = "my-security-dual.wks.in"

u-boot用の設定

u-boot scriptをraucに対応させる。
raucでu-bootを使う場合には起動時の面管理をu-boot環境変数BOOT_ORDERで行っている。そのため、以下のファイルをベースにBOOT_ORDERの値で起動面を切り替えるスクリプトを追加する。
https://github.com/rauc/meta-rauc-community/blob/master/meta-rauc-raspberrypi/recipes-bsp/rpi-u-boot-scr/files/boot.cmd.in

作成したものは以下。
BOOT_ORDERがAの場合はkernelをmmc 0:2から取り出しmmcblk0p5をrootfsとして使用する。
BOOT_ORDERがBの場合はkernelをmmc 0:3から取り出しmmcblk0p6をrootfsとして使用する。

test -n "${BOOT_ORDER}" || setenv BOOT_ORDER "A B"
test -n "${BOOT_A_LEFT}" || setenv BOOT_A_LEFT 3
test -n "${BOOT_B_LEFT}" || setenv BOOT_B_LEFT 3
test -n "${BOOT_DEV}" || setenv BOOT_DEV "mmc 0:1"

setenv bootpart
setenv raucslot
setenv kernel_addr_r 0x4000000

for BOOT_SLOT in "${BOOT_ORDER}"; do
  if test "x${bootpart}" != "x"; then
    # skip remaining slots
  elif test "x${BOOT_SLOT}" = "xA"; then
    if itest ${BOOT_A_LEFT} -gt 0; then
      setexpr BOOT_A_LEFT ${BOOT_A_LEFT} - 1
      echo "Found valid RAUC slot A"
      setenv bootpart "/dev/mmcblk0p5"
      setenv raucslot "A"
      setenv BOOT_DEV "mmc 0:2"
    fi
  elif test "x${BOOT_SLOT}" = "xB"; then
    if itest ${BOOT_B_LEFT} -gt 0; then
      setexpr BOOT_B_LEFT ${BOOT_B_LEFT} - 1
      echo "Found valid RAUC slot B"
      setenv bootpart "/dev/mmcblk0p6"
      setenv raucslot "B"
      setenv BOOT_DEV "mmc 0:3"
    fi
  fi
done

if test -n "${bootpart}"; then
  setenv bootargs "${bootargs} root=${bootpart} rauc.slot=${raucslot}"
  saveenv
else
  echo "No valid RAUC slot found. Resetting tries to 3"
  setenv BOOT_A_LEFT 3
  setenv BOOT_B_LEFT 3
  saveenv
  reset
fi

setenv bootargs "coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_compat_alsa=0 snd_bcm2835.enable_hdmi=1  smsc95xx.macaddr=DC:A6:32:6D:32:59 vc_mem.mem_base=0x3ec00000 vc_mem.mem_size=0x40000000  dwc_otg.lpm_enable=0 console=ttyS0,115200 root=${bootpart} rauc.slot=${raucslot} rootfstype=ext4 rootwait debug verbose"
fatload ${BOOT_DEV} ${kernel_addr_r} @@KERNEL_IMAGETYPE@@
saveenv
@@KERNEL_BOOTCMD@@

Linux kernelの設定

linux kernelをデプロイ用のディレクトリに格納する設定を行う。
local.confに以下の設定を加える。

IMAGE_BOOT_FILES:append = " fitImage-my-initramfs-raspberrypi4-64-raspberrypi4-64"

マウント設定

fstabを以下のように編集してbase-filesレシピで取り込む。

# stock fstab - you probably want to override this with a machine specific one

/dev/root            /                    auto       defaults              1  1
proc                 /proc                proc       defaults              0  0
devpts               /dev/pts             devpts     mode=0620,ptmxmode=0666,gid=5      0  0
tmpfs                /run                 tmpfs      mode=0755,nodev,nosuid,strictatime 0  0
tmpfs                /var/volatile        tmpfs      defaults              0  0

# uncomment this if your device has a SD/MMC/Transflash slot
#/dev/mmcblk0p1       /media/card          auto       defaults,sync,noauto  0  0

/dev/mmcblk0p1       /boot                auto       defaults              1  1
tmpfs                /mnt                 tmpfs      defaults              0  0

デフォルトの設定に加えて以下の設定をしている。 - /dev/mmcblk0p1を/bootにマウント
raucがuboot.env書き換えるため、uboot.envの入っているこのパーティションlinuxから見えるようにしておく必要がある。
- /mntにtmpfsをマウント
raucが/mnt下を使用するためwriteできるようにしておく必要がある。

rauc用鍵の作成

meta-raucに用意されているツールを使用する。
以下のreadmeの通りに作業して鍵を作成し、自身のレイヤに取り込んだ。
https://github.com/rauc/meta-rauc/blob/kirkstone/scripts/README

$ ./openssl-ca.sh
$ cp openssl-ca/dev/ca.cert.pem ../../../meta-my-raspberrypi/meta-my-security/recipes-core/rauc/files/
$ cp openssl-ca/dev/development-1.cert.pem ../../../meta-my-raspberrypi/meta-my-security/recipes-core/bundles/files/
$ cp openssl-ca/dev/private/development-1.key.pem ../../..
/meta-my-raspberrypi/meta-my-security/recipes-core/bundles/files/

アップデート設定

バイスのどの領域をアップデートするかの設定は以下に記述してある。
https://github.com/aimdevel/meta-my-raspberrypi/blob/main/meta-my-security/recipes-core/rauc/files/system.conf
ここで指定したスロット名やtypeはupdate-bundleを作成する際に使用する。

[system]
compatible=raspberrypi4-64
bootloader=uboot                    <== u-bootを使うことを指定。
data-directory=/var/data

[keyring]
path=/etc/rauc/ca.cert.pem          <== 鍵の場所を指定
 
[slot.kerneldir.0]                  <== A面のkerneldirスロットの設定
device=/dev/mmcblk0p2
type=vfat
parent=rootfs.0

[slot.kerneldir.1]                  <== B面のkerneldirスロットの設定 
device=/dev/mmcblk0p3
type=vfat
parent=rootfs.1

[slot.rootfs.0]                     <== A面のrootfsスロットの設定
device=/dev/mmcblk0p5
type=raw
bootname=A

[slot.rootfs.1]                     <== B面のrootfsスロットの設定
device=/dev/mmcblk0p6
type=raw
bootname=B

update-bundleの作成

raucのアップデートに使用する資材をまとめたものである.raucbファイルの設定は以下のディレクトリにあるファイルに書かれている。
https://github.com/aimdevel/meta-my-raspberrypi/tree/main/meta-my-security/recipes-core/bundles

  • kernel-dir.bb
    このファイルはlinux kernelをvfatのファイルに格納する処理を行っている。
    作成したvfatファイルはraucbを作成するとき使用する。

  • update-bundle.bb
    このファイルでアップデートの詳細を記載する。
    内容は以下である。

DESCRIPTION = "RAUC bundle generator"

FILESEXTRAPATHS:prepend := "${THISDIR}/files:"

DEPENDS = "linux-raspberrypi"

inherit bundle

RAUC_BUNDLE_COMPATIBLE = "${MACHINE}"
RAUC_BUNDLE_VERSION = "v20241104"
RAUC_BUNDLE_DESCRIPTION = "RAUC Demo Bundle"

RAUC_BUNDLE_FORMAT = "verity"

RAUC_BUNDLE_SLOTS = "rootfs kerneldir"

RAUC_SLOT_kerneldir = "kernel-dir"
RAUC_SLOT_kerneldir[file] = "kernel-dir.vfat"
RAUC_SLOT_kerneldir[type] = "file"
RAUC_SLOT_kerneldir[fstype] = "vfat"

RAUC_SLOT_rootfs = "core-image-base"
RAUC_SLOT_rootfs[file] = "core-image-base-raspberrypi4-64.ext4.verity"
RAUC_SLOT_rootfs[rename] = "rootfs.img"

RAUC_KEY_FILE ?= "${THISDIR}/files/development-1.key.pem"
RAUC_CERT_FILE ?= "${THISDIR}/files/development-1.cert.pem"

RAUC_SLOT_*でアップデートするスロットを指定する。このスロットとはデバイス内で定義されたどの領域をアップデートするかを表している。これはアップデート設定の内容と対応させる必要がある。
そしてスロットごとにどのような資材をインストールするかをRAUC_SLOT_*[file]RAUC_SLOT_*[type]RAUC_SLOT_*[fstype]で指定する。ここの設定はもっと良い書き方があるかもしれない。
先ほど説明したkernel-dirで作成したvfatファイルもここで指定することでraucbに取り込んでいる。

妥協した点

  • bootパーティションの内容
    ラズパイの起動に必要なファイルがどれなのか調べてないので、不要なファイルも含まれている。
  • kernelパーティションAの内容
    yoctoで格納するファイルを指定する方法がわからなかったので、bootパーティションと同じ内容。
    アップデートすればクリアされる。
  • dm-cryptの鍵置き場
    sdカード上に鍵をおいてあるが、本当はeFuseなどのsdカード以外の領域おかなければならない。
    このままだとsdカードを持ち去られたら普通に復号されてしまう。

終わりに

kernelも含めてラズパイのアップデートを試した際の設定を説明した。
アップデート時にデバイスに設定するべき大まかな項目はわかったので、さらに細かい設定を見ていくか、それかOTAを試すべくraucbを配信するサーバを立てるかなど次に何をやるか考えたい。

Linuxセキュリティ機能を盛り込んだラズパイのアップデート手順

初めに

前回の記事で作成したLinuxのセキュリティ機能を盛り込んだラズパイをアップデートできるようにしていく。 aimdevel.hatenablog.com 細かい話は長くなりそうなのでこの記事では概要と動かし方だけを説明し、設定値の説明は別の記事にまとめる。

環境

  • ubuntu 24.04 on wsl
  • yocto kirkstone
  • raspberrypi4B

作ったもの

AB面アップデートのシステムを構築する。
AB面アップデートとは、起動面と待機面を持ちアップデート処理時に待機面に新しいデータを書き込み、次の起動時はアップデートした待機面を起動面として使用するような方式である。
このAB面方式をとる利点は以下がある。

  • システムが止まる時間を短くできる
  • アップデート失敗した場合にも、アップデート前の面で起動することができる

アップデートツール

今回はraucを使用する。
raucとは組み込みLinuxのためのアップデートツールである。詳細は以下。 https://rauc.readthedocs.io/en/latest/

概要図

以下の図のように、linux kernelとrootfsを更新し、ユーザ固有のデータが格納された領域はそのまま残すようにアップデートする。

ユーザデータを格納するパーティションはアップデートで更新しないことを表す。 u-bootでどちらの面で起動するかを判定する。

パーティション構成

name size format description
mmcblk0p1 256MB vfat ブートパーティション。u-bootを格納
mmcblk0p2 128MB vfat linuxを格納。アップデートA面
mmcblk0p3 128MB vfat linuxを格納。アップデートB面
mmcblk0p4 none none 拡張パーティション
mmcblk0p5 500MB none rootfs。この領域をdm-verityで検証する。アップデートA面
mmcblk0p6 500MB none rootfs。この領域をdm-verityで検証する。アップデートB面
mmcblk0p7 500MB ext4 書き込み用暗号化領域。初回起動時にdm-cryptで暗号化する。アップデート後もデータが保持される領域として使用する。
mmcblk0p8 20MB ext4 dm-crypt用ファイル置場。キーファイルや初回起動フラグなど。

ブート~アップデート~再起動の流れ

  1. u-bootを起動
  2. u-bootが環境変数とboot.scrを使ってA面を起動面に選択。
  3. A面用のパーティションに格納されたLinux+initramfsで起動
  4. initramfs内で以下の流れでパーティションをマウント
    1. dm-veriryでrootfsを検証、マウント
    2. dm-cryptで暗号化領域をマッピング、マウント
    3. overlayfsで暗号化領域をrootfsの上(?)にマウント
  5. switch_rootで検証したrootfsで起動
  6. raucでアップデートを以下の手順で実行
    1. アップデート用のファイルを書き込み可能領域に格納
    2. B面にデータを書き込むコマンドを実行
  7. 再起動
  8. u-bootを起動
  9. u-bootが環境変数とboot.scrを使ってB面を起動面に選択
  10. B面用のパーティションに格納されたLinux+initramfsで起動
  11. この後はA面の起動と同じ

上記を目指して作ったものが以下。
https://github.com/aimdevel/meta-my-raspberrypi/tree/20241117
ざっくりと内容を説明する。

使い方

以下のようにkasを使ってビルドできるはず。

$ mkdir ~/workspace && cd ~/workspace
$ git clone https://github.com/aimdevel/meta-my-raspberrypi.git -b 20241117
$ kas build --target core-image-base meta-my-raspberrypi/kas-my-security.yml 
$ kas build --target update-bundle meta-my-raspberrypi/kas-my-security.yml 

ビルドに成功すると、 ~/workspace/build/tmp/deploy/images/raspberrypi4-64/ディレクトリにcore-image-base-raspberrypi4-64.wic.bz2update-bundle-raspberrypi4-64.raucbができている。 core-image-base-raspberrypi4-64.wic.bz2をsdカードに書き込んでラズパイを起動できる。
update-bunble.raucbはアップデートの際に使用するrauc用のファイル。
起動後は、ユーザ名rootでログインできる。

動作確認

  • 起動時の面管理の状態をraucコマンドで確認する。
    rootfs.0がactiveになっていることを確認できる。
$ rauc status
=== System Info ===
Compatible:  raspberrypi4-64
Variant:
Booted from: rootfs.0 (A)

=== Bootloader ===
Activated: rootfs.0 (A)

=== Slot States ===
o [rootfs.1] (/dev/mmcblk0p6, raw, inactive)
        bootname: B
        boot status: good
    [kerneldir.1] (/dev/mmcblk0p3, vfat, inactive)

x [rootfs.0] (/dev/mmcblk0p5, raw, booted)
        bootname: A
        boot status: good
    [kerneldir.0] (/dev/mmcblk0p2, vfat, active)
  • linux起動後にワークフォルダにraucbファイルを配置する sshサーバが起動しているはずなので、scpなどでraucbファイルを送り込む。
    ここでは/home/root/に配置したものとする。
  • rauc installでアップデートする
$ rauc install update-bundle-raspberrypi4-64.raucb
  • reboot
  • 起動後のマウント状態をraucコマンドで確認する。rootfs.1がactiveに変わっていることが確認できるはず。
$ rauc status
=== System Info ===
Compatible:  raspberrypi4-64
Variant:
Booted from: rootfs.1 (B)

=== Bootloader ===
Activated: rootfs.1 (B)

=== Slot States ===
x [rootfs.1] (/dev/mmcblk0p6, raw, booted)
        bootname: B
        boot status: good
    [kerneldir.1] (/dev/mmcblk0p3, vfat, active)

o [rootfs.0] (/dev/mmcblk0p5, raw, inactive)
        bootname: A
        boot status: bad
    [kerneldir.0] (/dev/mmcblk0p2, vfat, inactive)

またユーザデータ、具体的には/etcと/homeと/varディレクトリの下にあるデータが消えていないことも確認できる。

終わりに

セキュリティ機能を有効にした組み込みLinuxのアップデートを試した。
dm-verityのhash treeをどこに持たせるかなど改善できそうなところはまだまだあると思うが、今回はひとまず動くところまで。
今回作成した設定は次回の記事で説明する予定。

Yoctoを使用してLinuxセキュリティ機能を組み合わせて動かしてみた

初めに

今まではラズパイで動かすファイルを学習のためにそれぞれ手動で作成していた。
さすがにいちいちファイルを置き換えたりするのが面倒になってきたので、いままで作っていたものをyoctoで一括で作成できるようにしていく。
たまにはyoctoも触らないと忘れてしまいそうだし。

環境

  • ubuntu 24.04 on wsl
    微妙にyocto kirkstoneの推奨環境ではないが、ビルドできているので気にせず使っていく。
  • yocto kirkstone
    このバージョンを使い慣れているので。
  • raspberrypi4B
    実機はラズパイ4。

作ったもの

下記の構成で起動するLinuxシステムをビルドするyoctoのレイヤを作成した。
作成したものは以下のリポジトリのmeta-my-securityディレクトリ内に格納してある。
GitHub - aimdevel/meta-my-raspberrypi at 20240817

以前作成したラズパイOSベースのものとはソフトのバージョンなどが異なるが、起動の際に実行するdm-verityやdm-cryptの処理は大体同じ。
ラズパイ特有の設定については、以下のサイトなどを参考に作成した。
https://agherzan.github.io/meta-raspberrypi-docs/extra-build-config.html

概要図

以下のようにSDカードの資材を使って起動するLinuxを作成する。

パーティション構成

name size format description
mmcblk0p1 510MB vfat ブートパーティション。u-bootやLinux kernelなどを格納
mmcblk0p2 --- none rootfs。この領域をdm-verityで検証する
mmcblk0p3 100MB none 使おうと思って作ったけど結局使わなかった。リザーブ
mmcblk0p4 none none 拡張パーティション
mmcblk0p5 500MB ext4 書き込み用暗号化領域。初回起動時にdm-cryptで暗号化する
mmcblk0p6 20MB ext4 dm-crypt用ファイル置場。キーファイルや初回起動フラグなど。

ブートの流れ

  1. u-bootを起動
  2. Linux+initramfsで起動
  3. initramfs内で以下の流れでパーティションをマウント
    1. dm-veriryでrootfsを検証、マウント
    2. dm-cryptで暗号化領域をマッピング、マウント
    3. overlayfsで暗号化領域をrootfsの上(?)にマウント
  4. switch_rootで検証したrootfsで起動

作成したものの内容をざっくりを説明する。

使い方

以下のようにkasを使ってビルドできるはず。

$ mkdir ~/workspace && cd ~/workspace
$ git clone https://github.com/aimdevel/meta-my-raspberrypi.git
$ kas build meta-my-raspberrypi/kas-my-security.yml

ビルドに成功すると、sdカードイメージ ~/workspace/build/tmp/deploy/images/raspberrypi4-64/core-image-base-raspberrypi4-64.wic.bz2ができているので、これをsdカードに書き込んでラズパイを起動できる。
起動後は、ユーザ名testerでログインできる。

起動確認

ログを見ると、以下のように各機能を使っていることがわかる。

  • initramfs
    u-bootでFIT Image内のramdiskをロードしている。
## Loading ramdisk from FIT Image at 04000000 ...
   Using 'conf-broadcom_bcm2711-rpi-4-b.dtb' configuration
   Trying 'ramdisk-1' ramdisk subimage
     Description:  my-initramfs
     Type:         RAMDisk Image
     Compression:  uncompressed
     Data Start:   0x04864eec
     Data Size:    16002232 Bytes = 15.3 MiB
     Architecture: AArch64
     OS:           Linux
     Load Address: unavailable
     Entry Point:  unavailable
     Hash algo:    sha256
     Hash value:   b5bd3e6eb7a902ce5260b103ac5800a7191fb465f6403ec677ad195713fdc887
   Verifying Hash Integrity ... sha256+ OK
  • dm-verity
    dm-verityを使用している際のログ。
    initramfsで動作している時に実行している。
[    4.391012] device-mapper: verity: sha256 using implementation "sha256-generic"

また、起動完了後に/dev/mapperを見ると、以下のようにrootfsマッピングされていることを確認できる。

raspberrypi4-64:~$ ls -l /dev/mapper/
crw-------    1 root     root       10, 236 Jan  1  1970 control
lrwxrwxrwx    1 root     root             7 Jan  1  1970 crypt-partition -> ../dm-1
lrwxrwxrwx    1 root     root             7 Jan  1  1970 rootfs -> ../dm-0
  • dm-crypt
    dm-verityと同様に、/dev/mapperに上記のようにcrypt-partitionマッピングされていることを確認できる。

  • overlayfs
    mountコマンドを実行すると、以下のように読み込み専用のrootfsの上に書き込み可能なcrypt-partitionディレクトリをマウントしていることを確認できる。

raspberrypi4-64:~$ mount | grep rw-partition
/dev/mapper/crypt-partition on /dev/rw-partition type ext4 (rw,relatime)
overlay on /home type overlay (rw,relatime,lowerdir=/rootfs/home,upperdir=/dev/rw-partition/home,workdir=/dev/rw-partition/overlayfs-work/home)
overlay on /var type overlay (rw,relatime,lowerdir=/rootfs/var,upperdir=/dev/rw-partition/var,workdir=/dev/rw-partition/overlayfs-work/var)
overlay on /etc type overlay (rw,relatime,lowerdir=/rootfs/etc,upperdir=/dev/rw-partition/etc,workdir=/dev/rw-partition/overlayfs-work/etc)

各設定の説明

Linuxのセキュリティ機能を使うために作成した箇所について説明する。

partitionの設定

sdカードのパーティション設定は、meta-my-security/wic/my-security.wks.in に記載してある。
meta-raspberrypiのwksファイルをベースに作成した。
上記のパーティション構成と同じになるように設定してある。

part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 4096 --size 20
part / --source rawcopy --ondisk mmcblk0 --sourceparams="file=${IMGDEPLOYDIR}/${DM_VERITY_IMAGE}-${MACHINE}.${DM_VERITY_IMAGE_TYPE}.verity"
part / --ondisk mmcblk0 --size 100M
part / --ondisk mmcblk0 --size 500M
part / --ondisk mmcblk0 --fstype=ext4 --size 20M

このファイルの書き方は以下のページを参考にした。
https://docs.yoctoproject.org/ref-manual/kickstart.html

また、この設定を使用するためにlocal.confに以下の設定を追加。

WKS_FILE = "my-security.wks.in"

u-boot用の設定

local.confにu-bootを使用することを宣言する。

## use u-boot on raspbeerypi
ENABLE_UART = "1"
RPI_USE_U_BOOT = "1"
PREFERRED_PROVIDER_virtual/bootloader = "u-boot"

RPI_USE_U_BOOTはラズパイ特有の設定。
さらにLinuxの起動コマンドを指定する。

KERNEL_BOOTCMD = "bootm 0x4000000"

次は、u-bootのconfigを変更して、FIT Imageを有効にする。
作成したファイルは、meta-my-security/recipes-bsp/u-boot/u-boot_%.bbappendmeta-my-security/recipes-bsp/u-boot/files/enable-fit.cfg。 それぞれ内容は以下。

  • u-boot_%.bbappend
    後述のenable-fit.cfgを使用するための設定。
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"

SRC_URI:append = " file://enable-fit.cfg"
  • enable-fit.cfg
    u-bootのFIT機能を有効にする設定。
CONFIG_FIT=y
CONFIG_SHA256=y

次はu-boot.scrを作成する設定。
関連ファイルは、meta-my-security/recipes-bsp/rpi-u-boot-scr/rpi-u-boot-scr.bbappendmeta-my-security/recipes-bsp/rpi-u-boot-scr/files/boot.cmd.in それぞれ内容は以下。

  • rpi-u-boot-scr.bbappend
    boot.cmd.inの内部を必要に応じて編集して、mkimageでu-bootで使用できるようにboot.scrを作成する。
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"

do_compile() {
    sed -e 's/@@KERNEL_IMAGETYPE@@/${KERNEL_IMAGETYPE}-${INITRAMFS_IMAGE}-${MACHINE}-${MACHINE}/' \
        -e 's/@@KERNEL_BOOTCMD@@/${KERNEL_BOOTCMD}/' \
        "${WORKDIR}/boot.cmd.in" > "${WORKDIR}/boot.cmd"
    mkimage -A ${UBOOT_ARCH} -T script -C none -n "Boot script" -d "${WORKDIR}/boot.cmd" boot.scr
}
fatload mmc 0:1 0x4000000 @@KERNEL_IMAGETYPE@@
if test ! -e mmc 0:1 uboot.env; then saveenv; fi;
setenv bootargs "coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_compat_alsa=0 snd_bcm2835.enable_hdmi=1  smsc95xx.macaddr=DC:A6:32:6D:32:59 vc_mem.mem_base=0x3ec00000 vc_mem.mem_size=0x40000000  dwc_otg.lpm_enable=0 console=ttyS0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait debug verbose"
saveenv
@@KERNEL_BOOTCMD@@

FIT Imageの読み込み、bootargsの設定、ブートの順に実行するようになっている。

FIT imageの設定

FIT imageを使用するために以下の設定をlocal.confに追加している。

## use fitimage
KERNEL_CLASSES = "kernel-fitimage"
KERNEL_IMAGETYPE = "fitImage"

initramfsの設定

yoctoでinitramfsを使用するためにはlocal.confに以下の設定を行う。

INITRAMFS_IMAGE = "my-initramfs"

このmy-initramfsとは、meta-my-security/recipes-core/images/my-initramfs.bbで定義しているイメージである。レシピの内容は以下。

include recipes-core/images/dm-verity-image-initramfs.bb

export IMAGE_BASENAME = "${MLPREFIX}my-initramfs"

PACKAGE_INSTALL:append = " \
    initramfs-module-cryptedoverlay \
    e2fsprogs \
    kernel-module-dm-crypt \
    kernel-module-overlay \
"

このレシピはmeta-securityのdm-verity-image-initramfs.bbをベースにして、dm-cryptやoverlayfsに必要なモジュールとツールを追加している。

dm-verityの設定

dm-verity関連の設定はlocal.confに以下のように記載している。

DISTRO_FEATURES:append = " security"
DISTRO_FEATURES:append = " integrity"
DM_VERITY_IMAGE = "core-image-base"
DM_VERITY_IMAGE_TYPE = "ext4"
IMAGE_CLASSES += "dm-verity-img"

DISTRO_FEATURESはmeta-securityを使用する際の指定。
DM_VERITY_IMAGEとDM_VERITY_IMAGE_TYPEでdm-verityで検証するイメージ名とそのフォーマットを指定し、IMAGE_CLASSESにdm-verity-imgを追加する。

また、meta-my-security/recipes-kernel/linux/linux-raspberrypi_%.bbappendに以下を記載することで、kernelのdm-verityを有効にしている。

KERNEL_FEATURES:append = " ${@bb.utils.contains("IMAGE_CLASSES", "dm-verity-img", " features/device-mapper/dm-verity.scc", "" ,d)}"

dm-cryptの設定

dm-cryptはinitramfs内のスクリプトで実行するようにしている。
実際に動作させるスクリプトmeta-my-security/recipes-core/initrdscripts/files/cryptedoverlayである。
中でもdm-cryptにかかわる部分は以下。

echo "start dm-crypt"
mkdir -p /dev/rw-partition
mkdir -p /dev/key-part
echo "mount key-part"
mount /dev/mmcblk0p6 /dev/key-part
if [ ! -e "/dev/key-part/dm-crypt-key" ];then
    # initial setup at first boot
    echo "create key at first boot"
    dd bs=512 count=4 if=/dev/urandom of=/dev/key-part/dm-crypt-key
    echo "cryptsetup"
    cryptsetup --type plain -q -d /dev/key-part/dm-crypt-key -s 512 --hash sha256 -c aes-xts-plain64 open /dev/mmcblk0p5 crypt-partition
    echo "format fs"
    mkfs.ext4 /dev/mapper/crypt-partition
    echo "mount mapped partition"
    mount /dev/mapper/crypt-partition /dev/rw-partition
    mount "create directories"
    cd /dev/rw-partition/
    mkdir home var etc overlayfs-work
    cd /dev/rw-partition/overlayfs-work/
    mkdir home var etc
    cd
    umount /dev/rw-partition
else
    cryptsetup --type plain -q -d /dev/key-part/dm-crypt-key -s 128 --hash sha256 -c aes-xts-plain64 open /dev/mmcblk0p5 crypt-partition
fi 
umount /dev/key-part

このスクリプトでは、初回起動時に鍵を作成し目的のパーティションを暗号化、フォーマットする。二回目以降の起動では、初回起動時に作成した鍵を使ってパーティションマッピングするようになっている。
ここで使用するツール類は上述したようにmeta-my-security/recipes-core/images/my-initramfs.bbで取り込むように定義している。

overlayfsの設定

overlayfsもdm-cryptと同様にinitramfs内のスクリプトで実行するようにしている。 処理はmeta-my-security/recipes-core/initrdscripts/files/cryptedoverlay内のdm-crypt処理の後に行うようになっていて、内容は以下。

# mount rw partition
mount /dev/mapper/crypt-partition /dev/rw-partition

# mount overlayfs
echo "mount overlayfs"
mount -t overlay overlay -olowerdir=${ROOTFS_DIR}/home,upperdir=/dev/rw-partition/home,workdir=/dev/rw-partition/overlayfs-work/home ${ROOTFS_DIR}/home
mount -t overlay overlay -olowerdir=${ROOTFS_DIR}/var,upperdir=/dev/rw-partition/var,workdir=/dev/rw-partition/overlayfs-work/var ${ROOTFS_DIR}/var
mount -t overlay overlay -olowerdir=${ROOTFS_DIR}/etc,upperdir=/dev/rw-partition/etc,workdir=/dev/rw-partition/overlayfs-work/etc ${ROOTFS_DIR}/etc

ユーザ設定

ログイン用のユーザを追加する。
testerという名前のユーザを追加する設定は、meta-my-security/recipes-core/images/core-image-base.bbappendに記載しており内容は以下である。
extrausersを指定して、 EXTRA_USERS_PARAMSにユーザを設定するコマンドを記述している。

inherit extrausers
EXTRA_USERS_PARAMS = "\
    useradd -p '' tester; \
"

その他

bootパーティションにFIT Imageを追加する設定をlocal.confに追記している。
ファイル名を固定値で書いてしまっているが、変数に置き換えたほうがいい気がしている。

IMAGE_BOOT_FILES:append = " fitImage-my-initramfs-raspberrypi4-64-raspberrypi4-64"

また、ここまでに紹介したlocal.confの設定はkas-my-security.ymlに記載してある。

終わりに

Linuxのセキュリティ機能を使ったシステムをyoctoで作成した。
やはりyoctoを使用すると簡単に作れるので楽だった。
次はラズパイのセキュリティ機能との組み合わせや、システムのアップデートの対応などの拡張を試してみたい。