/home/tnishinaga/TechMEMO

日々行ったこと、面白かったことを書き留めます。

Rockstor をインストール時に暗号化してリモートから鍵入力できるようにする話

NASLinuxディストリビューションのRockstorを使ってお家のNASを作り直したので、そのときにやったことを備忘録として書き残しておきます。

再構築の目的

私は以前からRockstorを入れたNASサーバーを運用していたのですが、以下の点が気になっていたのでOSごとインストールし直しを行いました。

  • UEFIでなくBIOSブートでインストールされている
  • Data diskが暗号化されていない
  • data diskの鍵を入れるOS diskも暗号化されていない
  • RAIDを組むためにData diskを2枚指したのにRAIDを組めていない

再構築の主目的は以下の2点になります。

  • RockstorのOSの入っているdisk(以降OS disk)含めて暗号化する
  • Dataを入れるdisk(以降data disk)をRAID1にする

なお、RockstorをUEFIブートするようインストールする件に関しては、途中で発生した問題をすべて解決できなかったので諦めました。詳細は最後におまけで記します。

暗号化の話

インストール

最近のインストーラーはとても親切なので、指示に従っているだけでDisk暗号化をしつつOSのインストールができました。 わからないことがあっても、RockstorはRHELディストリビューションをベースとしているので、FedoraCentOSのドキュメントを参考にすれば良いでしょう。

ディスク暗号化のリモート解除

さて、ここが私がこの備忘録を書いたきっかけになります。

インストール後、実際に起動してみて困ったのが起動時のディスク暗号化解除方法です。 Rockstorでディスク暗号化を行いつつインストールを行うと、起動時にディスク暗号化を解除するためのパスコード入力を求められます。 しかし、うちのサーバーマシンは基本的にディスプレイとキーボードを外して運用していることに加え、頻繁に電源をON//OFFするので、毎回起動時にパスワード入力を求められると不便です。

これを解決するために、はUSBメモリに鍵を入れ、起動時に自動で復号する方法やSSH経由でリモートから鍵入力をする方法がありますが、今回は後者を試してみました。この方法を使うメリットとしては、復号鍵が対象から分離されることと、USBメモリなど追加で必要なものがないので気軽に試せる点があるでしょう。

基本的な作業手順は以下のサイトのStep6以降を進めるだけです。

https://www.vultr.com/docs/install-and-setup-centos-7-to-remotely-unlock-lvm-on-luks-disk-encryption-using-ssh#Step_6__Install_Dracut_Crypt_SSH

こちらも参考になります。

https://github.com/dracut-crypt-ssh/dracut-crypt-ssh

Rockstorのマシンにrootでログインした後、dracut-crypt-sshパッケージを入れます。

sudo yum install wget -y
sudo wget -O /etc/yum.repos.d/rbu-dracut-crypt-ssh-epel-7.repo https://copr.fedorainfracloud.org/coprs/rbu/dracut-crypt-ssh/repo/epel-7/rbu-dracut-crypt-ssh-epel-7.repo
sudo yum install dracut-crypt-ssh -y

次に/etc/default/grubを開いてgrubのエントリを修正し、起動時にIPアドレスを得るように設定します。

アドレス取得をDHCPで行う場合は、GRUB_CMDLINE_LINUX="crashkernel=autord.luks.uuid= の間に rd.neednet=1 ip=dhcp を入れれば設定完了です。

static IPを割り当てたい方は以下を参照してください。

https://wiki.archlinux.jp/index.php/Dm-crypt/%E7%89%B9%E8%A8%98%E4%BA%8B%E9%A0%85#root_.E3.81.AA.E3.81.A9.E3.81.AE.E3.83.91.E3.83.BC.E3.83.86.E3.82.A3.E3.82.B7.E3.83.A7.E3.83.B3.E3.81.AE.E3.83.AA.E3.83.A2.E3.83.BC.E3.83.88.E8.A7.A3.E9.99.A4

設定後のGRUB_CMDLINE_LINUXの行はこんな感じになります。

GRUB_CMDLINE_LINUX="crashkernel=auto rd.neednet=1 ip=dchp rd.luks.uuid=luks-xxxx rhgb quiet"

設定が完了したら以下のコマンドでgrubのconfigを生成します。

sudo grub2-mkconfig -o /etc/grub2.cfg

次にinitramfs内に入れるSSHの設定を記入していきます。

/etc/dracut.conf.d/crypt-ssh.conf を開いて以下を追記します。

dropbear_acl="/etc/dropbear/keys/authorized_keys"
dropbear_ecdsa_key="/etc/dropbear/keys/ssh_ecdsa_key"
dropbear_rsa_key="/etc/dropbear/keys/ssh_rsa_key"

ファイルを保存したら、ここで設定した場所に新しいSSH鍵を生成します。 この鍵は起動時にディスク復号パスワードを受け取るためのSSH通信に利用されます。

sudo mkdir -p /etc/dropbear/keys/; sudo chmod /etc/dropbear/keys/
sudo ssh-keygen -t ecdsa -f /etc/dropbear/keys/ssh_ecdsa_key
sudo ssh-keygen -t rsa -f /etc/dropbear/keys/ssh_rsa_key
sudo chmod 400 /etc/dropbear/keys/*_key; sudo chmod 444 /etc/dropbear/keys/*.pub

次に /etc/dropbear/keys/authorized_keys を開き、ディスク復号鍵を送るための、接続元マシンの公開鍵を記述します。ここで記述する鍵はrsaの公開鍵である必要があります。dropbearを使う場合はed25519鍵でアクセスできないようです。ここ、ドハマりポイントでした。

最後にdracutコマンドでinitramfsを生成して再起動すれば設定完了です。

sudo dracut -f && sudo reboot

再起動後、サーバーにユーザーroot、ポート222で接続し、console_authコマンド実行後にディスク復号のパスワードを入れればOSが立ち上がります。

なお、このアンロックの手順も自動化できるようなのですが、とりあえず今日はここまでで。詳しい話は以下を参照。

https://github.com/dracut-crypt-ssh/dracut-crypt-ssh#34-unlocking-using-the-unlock-command

data disk暗号化

data diskの暗号化は公式のドキュメントがあるのでこちらを参照して作成してください。

http://rockstor.com/docs/luks/luks.html

RAIDを組む場合は先にディスク暗号化を済ませてからpool割当のところで設定すれば作れます。詳しくは以下参照です。

http://rockstor.com/docs/pools-btrfs.html#redundancy-profiles

おまけ: RockstorがUEFIブートでインストールできない話

結論から話せば、少なくとも2019年2月ごろのRockstorはUEFIブートからインストールができませんでした。以下理由を記しますが、メモをとっていなかったため誤りが含まれるかもしれません。

まず、インストーラーのISOをそのままUSBメモリに書き込むとUEFIブートしません。

これはfatでフォーマットしたUSBメモリにISOの中身を書き込むと回避できますが、今度は立ち上がったKernelがrootfsをマウントできず失敗します。

これはどうやらrootfsのラベル指定がUSBメモリのラベルを指していないことが原因のようで、grub.cfgを書き換えてUSBメモリのラベルで参照できるようにすると解決します。

この修正でインストーラーが立ち上がるところまで進みますが、インストール途中にエラーが起きて先に進めない問題が発生したため、解決を諦めました。

おしまい

おしまい。

Chisel + PYNQでLチカ(お正月FPGAあそび)

お正月なので普段やらないことをやろうと思い、買ってから2年ほど放置してしまっていたPYNQ-Z1を使ってFPGAを触ってみました。

RISC-Vで遊びたい」という気持ちと「FPGAをさわってみたい」という気持ちがあったので、「PYNQでRISC-Vを動かしてみる」ことを最終目標にしました。 しかし、これではお正月休みだけでは終わらないので、このお休みの間は「PYNQでChiselを使ってLチカする」のをゴールとしてやってみました。

今回はそこで何やったかのメモ的な記事です。

なお、私はFPGAScalaもChiselもほぼ初挑戦です。

Vivado setup

PYNQはDigilent社の作ったFPGAボードです。

www.pynq.io

PYNQにはXilinx社のZynqというFPGAが乗っているので、Xilinx社のVivadoという開発環境で開発を行います。 以下のサイトからアカウントを作ってインストールします。WebPackライセンスを選択すればいろいろ制限はありますが無料で使えます。

japan.xilinx.com

インストールが終わったら以下からPYNQ向けのボードファイルをもらってきてインストールします。

https://github.com/cathalmccabe/pynq-z1_board_files/raw/master/pynq-z1.zip

$ wget https://github.com/cathalmccabe/pynq-z1_board_files/raw/master/pynq-z1.zip
$ unzip pynq-z1.zip
$ sudo cp -r pynq-z1 /tools/Xilinx/Vivado/2018.3/data/boards/board_files

PS + PL でLチカ

環境設定が終わったので、組み込み版HelloWorldであるLチカをやってみます。

PYNQボード向けの入門ドキュメントは少ないので、だいたい同じ構成のZyboというボード向けの初心者用ドキュメントを参考に、一部読み替えつつLチカをしてみます。

Getting Started with Zynq [Reference.Digilentinc]

PYNQボードに乗っているZynqというFPGAチップには、ARMのCPUコア(PS: Processing System)とFPGA(PL: Programmable Logic)が1つのチップに乗っています。 今回参考にするドキュメントの例では、PL部に用意したGPIOをPS部のプログラムから制御して、スライドスイッチの状態に応じてLEDを点灯させるというものになります。

Zybo向けに書かれたこのドキュメントをPYNQで用いるための変更点としては、PYNQにはスライドスイッチが2つしか無いので、手順4.3で設定するスイッチをswts_2bitsに設定する必要があります。

PL部のみでLチカ

次にPYNQをPS部を用いず普通のFPGAボードとして利用する方法が知りたいので、以下のサイトを参考にPL部のみでLチカをしてみたいと思います。

qiita.com

こちらのドキュメントもZYNQ向けなので、「ピンアサインをする」のところでクロックなどが接続されているピンがPYNQと異なります。そのため、以下のマニュアルを参考に変更を加えます。

https://reference.digilentinc.com/_media/reference/programmable-logic/pynq-z1/pynq-rm.pdf

「11 Clock Sources」と「12 Basic I/O」を読むと、クロックソースとLEDのピン配置が以下の表のようになっていることがわかるので、設定します。

Parts FPGA Pin
125MHz Clock H16
LED LD0 R14
LED LD1 P14
LED LD2 N16
LED LD3 M14

回路がうまく動けば、1秒ごとにLEDが4つとも点滅します。

Chiselを使ったLチカ

RISC-VのコアはChiselという、ScalaDSLとして実装された言語を用いて書かれています。 このChiselの書き方を学ぶため、まずはこのChiselを使って先程のPL部だけで行うLチカの回路を作ってみたいと思います。

まずChiselから実際のボード上で動く回路を作るまでの流れが知りたかったので調べてみます。 Chiselのコードをビルドすると、中間表現を経由してVerilogのコードが得られます。実際のボード上で動かすためにはこの生成したVerilogをvivadoに取り込んでピンアサインなどを設定してビルドする必要が有るようです。

詳細についてはmskysphinzさんのブログ記事が参考になりました(ありがとうございます)。

msyksphinz.hatenablog.com

次にプロジェクトの開始の際には、chisel-templateというリポジトリをベースに行うのが良いようです。

github.com

chisel-remplateにはGCDを求める回路が最初から書かれています。 最初は簡単のためメソッド名等はそのままに中身だけ書き換えてLチカのコードを書いていきます。

src/main/scala/gcd/GCD.scala

Verilogで書いていたLチカのコードをChiselで書き直します。 Clockは暗黙の引数として有るようなので、書かなくて良いようです。 onoff変数の値をLED4つに反映させる方法がわからなかったので、LED0にだけonoffの値を入れるようにしています。

// src/main/scala/gcd/GCD.scala

package gcd

import chisel3._

/**
  * Compute GCD using subtraction method.
  * Subtracts the smaller from the larger until register y is zero.
  * value in register x is then the GCD
  */
class GCD extends Module {
  val io = IO(new Bundle {
    val outputLED     = Output(UInt(4.W))
  })
  //  parameter CNT_1SEC = 27'd124999999;  // 125MHz clk for 1sec
  val CNT_1SEC = RegInit(124999999.U(27.W))
  //  reg [26:0] cnt = 27'd0;
  val cnt  = RegInit(0.U(27.W))
  // reg onoff = 1'd0;
  val onoff  = RegInit(false.B)

  // if (cnt == CNT_1SEC) begin
  when(cnt === CNT_1SEC) {
    // cnt <= 27'd0;
    // onoff <= ~onoff;
    cnt := 0.U
    onoff := ~onoff
  } .otherwise {
    // cnt <= cnt + 27'd1;
    cnt := cnt + 1.U
  }

  // assign LED = {onoff, onoff, onoff, onoff};
  io.outputLED := onoff
}

src/test/scala/gcd/GCDUnitTest.scala

回路のテストコードを書きます。

サンプルではpokeを使って回路に入力を設定しているものがよく見られますが、この回路は入力がクロックしか無いので、pokeで入力設定を行わなくても良いようです。

stepを進めるとクロックが入力されてカウンタが上がっていきます。 今回125MHzでLEDの出力を反転させるので、stepで125,000,000進めた後にexpectを使って出力が変わっているかをチェックします。

class GCDUnitTester(c: GCD) extends PeekPokeTester(c) {
  private val gcd = c

  // return 0 when clk counter == 0
  expect(gcd.io.outputLED, 0)

  // return 0 when clk counter == 1
  step(1)
  expect(gcd.io.outputLED, 0)

  // return 1 when clk counter == 124999999
  step(125000000 - 1)
  expect(gcd.io.outputLED, 1)

  // return 1 when clk counter == 0
  step(1)
  expect(gcd.io.outputLED, 1)

  // return 0 when clk counter == 124999999
  step(125000000 - 1)
  expect(gcd.io.outputLED, 0)
}

src/test/scala/gcd/GCDMain.scala

そのままもテストはできますがVerilogファイルの出力ができなかったので、chisel-wikiを参考にMainメソッドに1行追加してVerilogのコードをビルドできるようにします。 以下のドキュメントを参考に、出力のための処理を追記します。

Frequently Asked Questions · freechipsproject/chisel3 Wiki · GitHub

object GCDMain extends App {
  iotesters.Driver.execute(args, () => new GCD) {
    c => new GCDUnitTester(c)
  }

  // 追記
  chisel3.Driver.execute(args, () => new GCD)
}

テスト

tutorialのREADME.mdに書いてあるとおりに以下のコマンドを実行するとテストが走ります。

$ sbt 'testOnly gcd.GCDTester -- -z Basic'

うまくいけばこんな感じのログが出てきます。

$ sbt 'testOnly gcd.GCDTester -- -z Basic'                                                                                                                                                                                                                                                                                                                                                                                  
[info] Loading settings from plugins.sbt ...
[info] Loading project definition from /home/tnishinaga/projects/chisel/chisel_ledblink/project
[info] Loading settings from build.sbt ...
[info] Set current project to chisel-ledblink (in build file:/home/tnishinaga/projects/chisel/chisel_ledblink/)
[info] Compiling 1 Scala source to /home/tnishinaga/projects/chisel/chisel_ledblink/target/scala-2.11/classes ...
[warn] there was one feature warning; re-run with -feature for details
[warn] one warning found
[info] Done compiling.
[info] [0.005] Elaborating design...
[info] [2.191] Done elaborating.
Total FIRRTL Compile Time: 512.2 ms
Total FIRRTL Compile Time: 201.5 ms
file loaded in 0.388108123 seconds, 13 symbols, 9 statements
[info] [0.002] SEED 1546794998929
test GCD Success: 5 tests passed in 250000005 cycles in 118.643097 seconds 2107160.15 Hz
[info] [118.613] RAN 250000000 CYCLES PASSED
[info] GCDTester:
[info] GCD
[info] Basic test using Driver.execute
[info] - should be used as an alternative way to run specification
[info] using --backend-name verilator
[info] running with --is-verbose
[info] running with --generate-vcd-output on
[info] running with --generate-vcd-output off
[info] ScalaTest
[info] Run completed in 2 minutes, 3 seconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 135 s, completed Jan 7, 2019 00:01:42 AM

Verilogコードの生成

Verilogのコード生成は以下のコマンドで行えました。 このコマンドがスマートな方法かはわかりませんが、動いたので良しとしています。どなたか良い方法をご存知の方は教えてください。

$ sbt 'test:runMain gcd.GCDMain --target-dir buildstuff --top-name GCDMain'

うまくビルドが終わればbuildstuffディレクトリ以下にGCDMain.vというファイルができているはずです。 機械が生成したコードなので、先程の人間の作ったコードに比べるとちと読みづらいです。

module GCD( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  output [3:0] io_outputLED // @[:@6.4]
);
  reg [26:0] cnt; // @[GCD.scala 19:21:@9.4]
  reg [31:0] _RAND_0;
  reg  onoff; // @[GCD.scala 21:23:@10.4]
  reg [31:0] _RAND_1;
  wire  _T_13; // @[GCD.scala 25:12:@11.4]
  wire  _T_15; // @[GCD.scala 27:14:@14.6]
  wire [27:0] _T_17; // @[GCD.scala 29:16:@18.6]
  wire [26:0] _T_18; // @[GCD.scala 29:16:@19.6]
  wire [26:0] _GEN_0; // @[GCD.scala 25:26:@12.4]
  wire  _GEN_1; // @[GCD.scala 25:26:@12.4]
  assign _T_13 = cnt == 27'h773593f; // @[GCD.scala 25:12:@11.4]
  assign _T_15 = ~ onoff; // @[GCD.scala 27:14:@14.6]
  assign _T_17 = cnt + 27'h1; // @[GCD.scala 29:16:@18.6]
  assign _T_18 = cnt + 27'h1; // @[GCD.scala 29:16:@19.6]
  assign _GEN_0 = _T_13 ? 27'h0 : _T_18; // @[GCD.scala 25:26:@12.4]
  assign _GEN_1 = _T_13 ? _T_15 : onoff; // @[GCD.scala 25:26:@12.4]
  assign io_outputLED = {{3'd0}, onoff}; // @[GCD.scala 33:16:@22.4]
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
`ifdef RANDOMIZE
  integer initvar;
  initial begin
    `ifdef INIT_RANDOM
      `INIT_RANDOM
    `endif
    `ifndef VERILATOR
      #0.002 begin end
    `endif
  `ifdef RANDOMIZE_REG_INIT
  _RAND_0 = {1{`RANDOM}};
  cnt = _RAND_0[26:0];
  `endif // RANDOMIZE_REG_INIT
  `ifdef RANDOMIZE_REG_INIT
  _RAND_1 = {1{`RANDOM}};
  onoff = _RAND_1[0:0];
  `endif // RANDOMIZE_REG_INIT
  end
`endif // RANDOMIZE
  always @(posedge clock) begin
    if (reset) begin
      cnt <= 27'h0;
    end else begin
      if (_T_13) begin
        cnt <= 27'h0;
      end else begin
        cnt <= _T_18;
      end
    end
    if (reset) begin
      onoff <= 1'h0;
    end else begin
      if (_T_13) begin
        onoff <= _T_15;
      end
    end
  end
endmodule

FPGAでの動作確認

PL部でのLチカを参考に、VerilogのコードをコピペしてVivadoでビルドを行います。 Chiselはreset用のpinも欲しがるので、私はとりあえずスライドスイッチのついているM20を設定しました。

PYNQにうまく書き込めれば、LEDのLD0だけがチカチカするはずです。

おもったこと

最後に、やってる途中に思ったことをつらつらと。

  • ChiselのまえにScala(基本文法とsbtつかったビルド方法)を勉強したほうが良さそう
  • Verilogを使った普通のFPGA開発も本一冊分くらいはやったほうが良さそう
  • RISC-Vの実機が触りたいなら10万出してhifive unleashed買うか、マイコンで良ければArty FPGAボードかHiFive1を買ったほうがよさそう

UEFIの呼び出し規則について

UEFIの呼び出し規則について調べたので忘れないようにメモしておきます。

呼び出し規則とバイナリフォーマット

UEFIのアプリケーションは以下の点が一般的なLinuxのアプリケーションと異なるため、Linux環境上でUEFIアプリをビルドするには解決の必要があります。

  • 関数呼び出し規則(calling convention)が異なる(x86, x86_64の場合)
  • 実行バイナリのファイル形式がPEフォーマット(UEFI Specification Version 2.5 Errata A pp.18 2.1.1 UEFI Images参照)

私のようにx86以外をターゲットとしてビルドする際は後者の解決が重要ですが、今回は前者について調べたことを記します。

呼び出し規則への対応

前者の呼び出し規則とは、関数を呼び出す際のレジスタの使い方などを決めているものです。 規則にはどのレジスタに引数を入れて渡すか、どのレジスタに戻り地を入れて返すかなどが決められているため、この規則が異なる関数をそのまま呼び出すことはできません。 この規則の違いがx86系のプロセッサ上で動作するUEFIアプリを作る際に問題となります。

x86 Linux向けにアプリをビルドすると、呼び出し規則はSystem V ABIに定められたものが使われます。 一方、UEFIではMS-ABIやMicrosoft x64 calling conventionと呼ばれるWindowsで使われる呼び出し規則を利用しているため、Linux等からUEFIアプリをビルドするためにはこの呼び出し規約の差異を解決する必要があります。 (UEFI Specification Version 2.5 Errata A pp.32 2.3.4.2 Detailed Calling Conventionsを参照。直接MS-ABIとは書いてはない)

私の知っている限りでは、この差異を解決する方法が2つあります。

  • MS-ABIでビルドする
  • ラッパー関数で呼び出し規則を修正する

MS-ABIでビルドする

前者はコンパイラにms-abiを用いるオプションを指定したり、attribute で指定することで解決を行います。

gcc.gnu.org

gnu-efiではコンパイラがms-abiに対応している場合、defineを使ってEFIAPIをつけた関数に __attribute__((ms_abi)) を適応してms-abiで呼び出すようにしています。

ラッパー関数で呼び出し規則を修正する

後者はコンパイラがms-abiを利用できない場合に利用される方法です。 gnu-efi ではuefi_call_wrapperというラッパー関数(中身はマクロ)を利用して、引数の数に合わせて引数の順序を入れ替える関数を選んで使用しています。

Linuxでも同じようなことをやっているようです(efi_call関数を参照)。

linux/efi_stub_64.S at 6f0d349d922ba44e4348a17a78ea51b7135965b1 · torvalds/linux · GitHub

なお、ARMやその他アーキテクチャでは基本的にLinuxと同様のABIを用いているので、ABIの変換作業は必要ありません。 実際にgnu-efiのラッパー関数も変換を行っていないようです。

gnu-efi / Code / [fc5af9] /inc/aarch64/efibind.h

以上です。 PEフォーマットの件は次回書きます。

Bus Blaster v3を買いました

かっちった

購入まで

先週Japan Technical Jamboree(Twitterでのハッシュタグ#celfjp )という、組み込みLinuxに関係する企業の方や趣味の方が集まる勉強会で、個人としてSynQuacerを個人マネーで購入して遊んでいることを報告してきました。

Japan Technical Jamboree 65 - eLinux.org

スライドはこちら

speakerdeck.com

SynQuacerにはJTAGデバッグ用の端子が搭載されているのですが、信号電圧が1.8Vなので私の愛用していたOlimexのARM-USB-TINY-H というJTAGアダプタが使えないという問題がありました。

この件についてJamboree内でBus Blaster v3を使うことをおすすめ*1されたので、さくっとSeeed Studioで購入しました。

商品のページはこちら。

www.seeedstudio.com

一緒にアクリルパネルをおすすめされたので、こちらも一緒に購入しました。

www.seeedstudio.com

お値段

気になるお値段は以下(2018/05/25時点)。

もの お値段(USD)
Bus Blaster v3 34.95
アクリルケース 3.00
送料(fedex) 20.72
合計 58.67

PayPal でアカウント登録して、PayPal経由で支払いしました。

支払った額は日本円で6,708円でした。

届くまで

  • 5月25日金曜日夕方に注文
  • 5月30日住所情報不足で配達不可のため修正
  • 5月31日到着

大体1週間で届きました。 私が住所を間違えなければも少し早く届いたと思います。

これから

このJTAGアダプタがRasPiに使えるかやSynQuacerにつなげる際のやり方などはこれから試します。 本日はこれにて。

*1:公式ドキュメントでもおすすめされている

Windows10とArch Linuxをファイルシステムを暗号化しながら入れた話 その1

あらすじ

新マシンとしてThinkPad X230の中古品と480GBのSSDを買ったので、セットアップを行いました。その時行ったことを備忘録として書いておこうと思います。

ハードの性能はこんな感じです。

name spec
Hardware ThinkPad x230
CPU Intel(R) Core(TM) i5-3320M CPU @ 2.60GHz
Memory 8GB
Storage SSD 480GB

OSのインストールは以下の希望を満たせるよう、進めることにします。

  • Windows & Linux dual boot
  • UEFIブート(not CSMモード)
  • Win,Linux どちらでも読み書きできる領域(ファイル交換用)を16GBがほしい
  • winもLinuxファイルシステムを暗号化したい(紛失後の情報流出の心配を減らしたい)
  • 暗号化されたswap領域も欲しい
  • サスペンドのみでハイバネートは使わない
  • Btrfs + snapper + snap-syncで差分バックアップ取れるようにしたい

一言でまとめるとすると「(UEFI + Windows 10 + Bitlocker(without TPM)) and (UEFI + Arch Linux + dm-crypt + Btrfs)」とかけばいいのでしょうか......? 結構大変です。

手順が長くなるので、以下の3つくらいに記事を分けたいと思います。

  • USBディスクからArchをインストールするまで
  • Archのデスクトップ環境を作るまで
  • Snapperで定期snapshot取得およびバックアップを設定するまで

Windowsのインストール

先にWindows 10を120GBのパーティションを作ってインストールします。

細かいインストール方法等は省略します。

UEFIの設定でTPMをDisableにすればBitlockerを用いて暗号化も可能です。 TPMを有効にしたまま行うのは難しそうです。

Linuxをインストール

パーティション作成

既にWindowsパーティションが4つでできているので、新たにrootfs(sda5)に298GiB、swap(sda6)に16GiB、ファイル交換用(sda7)に16GiBのパーティションを作ります。

fdisk /dev/sda

完成後はこんな感じになります。

partition size type
/dev/sda1 499M Windows recovery environment
/dev/sda2 100M EFI System
/dev/sda3 16M Microsoft reserved
/dev/sda4 116G Microsoft basic data(Windows10 OS)
/dev/sda5 298G Linux filesystem(rootfs)
/dev/sda6 16G Linux swap
/dev/sda7 16G Microsoft basic data(ファイル交換用)

最後のsda7だけ暗号化しないのでexfatでフォーマットしておきます。

mkfs.exfat /dev/sda7

ファイルシステム作成とマウント

rootfsの暗号化

以下を参考に、dm-crypt/LUKSを使ってrootfsを暗号化します。

https://wiki.archlinux.jp/index.php/Dm-crypt/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E5%85%A8%E4%BD%93%E3%81%AE%E6%9A%97%E5%8F%B7%E5%8C%96#Btrfs_.E3.82.B5.E3.83.96.E3.83.9C.E3.83.AA.E3.83.A5.E3.83.BC.E3.83.A0.E3.81.A8.E3.82.B9.E3.83.AF.E3.83.83.E3.83.97

sudo  cryptsetup -s 512 luksFormat /dev/sda5

Are you sure? (Type uppercase yes): と聞かれるので、大文字でYESと答えます。 次にパスワードを聞かれるので、入力してください。。 これで暗号化完了です。

暗号化を解除するには、以下のコマンドを実行します。

cryptsetup open --type luks /dev/sda5 cryptroot

これで /dev/mapper/cryptroot に復号されたデバイスがマップされます。

rootfsパーティション作成

復号されたデバイス上にbtrfsのファイルシステムを作ります。

mkfs.btrfs /dev/mapper/cryptroot

マウント

compress=zstd をつけて圧縮を有効にしながら、rootfsを/mnt にマウントします。

mount -o compress=zstd /dev/mapper/cryptroot /mnt

subvolume の作成

Snapperで使える推奨ファイルシステムレイアウトを作ります。 推奨レイアウトについては以下を参照してください。

https://wiki.archlinux.jp/index.php/Snapper#.E6.8E.A8.E5.A5.A8.E3.83.95.E3.82.A1.E3.82.A4.E3.83.AB.E3.82.B7.E3.82.B9.E3.83.86.E3.83.A0.E3.83.AC.E3.82.A4.E3.82.A2.E3.82.A6.E3.83.88

cd /mnt
sudo btrfs subvolume create @
sudo btrfs subvolume create @home
sudo btrfs subvolume create @log

一緒にsnapper向けのsubvolumeも作ります。

sudo btrfs subvolume create @snapshots
sudo btrfs subvolume create @snapshots/snapshots_root
sudo btrfs subvolume create @snapshots/snapshots_home

一旦マウントし直します。

cd 
umount /mnt
mount -o compress=zstd,subvol=@ /dev/mapper/cryptroot /mnt
mkdir /mnt/home
mount -o compress=zstd,subvol=@home /dev/mapper/cryptroot /mnt/home
mkdir -p /mnt/var/log
mount -o compress=zstd,subvol=@log /dev/mapper/cryptroot /mnt/var/log
cd /mnt
mkdir -p var/cache/pacman/
btrfs subvolume create var/cache/pacman/pkg
btrfs subvolume create var/abs
btrfs subvolume create var/tmp
btrfs subvolume create srv

bootのマウント

bootをマウントします。

mkdir /mnt/boot
mount /dev/sda2 /mnt/boot

Archのセットアップとインストール

システムクロック更新

timedatectl set-ntp true

ミラー選択

/etc/pacman.d/mirrorlist を編集します。

sed -i -e "s/Server/#Server/" /etc/pacman.d/mirrorlist
nano /etc/pacman.d/mirrorlist

JAPANのミラーだけコメントアウトから外してください。

base systemのインストール

cd /mnt
pacstrap /mnt base base-devel

fstabの反映

マウント情報をfstabに反映します。

genfstab -U /mnt >> /mnt/etc/fstab

暗号化されたswap領域の作成とマウント設定

swapはRAMのデータが一時的に退避される場所です。 なので、ここが暗号化されていないとRAMにあるデータが一部読み取れてしまうかもしれないので、暗号化します。

ハイバネートをすることを考えなければ、swapの暗号化は非常に簡単です。 以下のドキュメントを参考に /mnt/etc/crypttab/mnt/etc/fstab を修正すれば、swapの暗号化は完了します。

https://wiki.archlinux.jp/index.php/Dm-crypt/%E3%82%B9%E3%83%AF%E3%83%83%E3%83%97%E3%81%AE%E6%9A%97%E5%8F%B7%E5%8C%96#suspend-to-disk_.E3.82.92.E4.BD.BF.E7.94.A8.E3.81.97.E3.81.AA.E3.81.84

まずswapパーティション(sda6)の先頭1Mをext2でフォーマットし、UUIDを得ます。

mkfs.ext2 -L cryptswap /dev/sda6 1M
blkid /dev/sda6

すると、UUIDとLABELが得られるので、どちらか好きな方を使って /mnt/etc/crypttab に暗号化の設定を追記します。

swap     LABEL=cryptswap  /dev/urandom  swap,offset=2048,cipher=aes-xts-plain64,size=256

これで起動時に暗号化されたswapが /dev/mapper/swap にマウントされるので、このswapを使うようにfstabに追記します。

/dev/mapper/swap  none   swap    defaults   0       0

なぜこのようにoffsetを使うかの理由は、以下を参照してください。

https://wiki.archlinux.jp/index.php/Dm-crypt/%E3%82%B9%E3%83%AF%E3%83%83%E3%83%97%E3%81%AE%E6%9A%97%E5%8F%B7%E5%8C%96#UUID_.E3.81.A8_LABEL

It is more reliable to identify the correct partition by giving it a genuine UUID or LABEL. By default that does not work because dm-crypt and mkswap would simply overwrite any content on that partition which would remove the UUID and LABEL too

swapの書き込み制限

sudo sh -c "echo 10 > /proc/sys/vm/swappiness"
sudo sh -c 'echo "vm.swappiness = 10" >> /mnt/etc/sysctl.conf'

swapの書き込み回数を減らしてSSDの寿命を伸ばします。

chroot

chrootでArchのrootfsを設定していきます。

arch-chroot /mnt

タイムゾーン設定

ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

locale設定

/etc/locale.gen を編集して以下のコメントを外します。

そして以下のコマンドを実行します。

locale-gen

最後にlocaleを設定します。

sudo sh -c 'echo LANG=en_US.UTF-8 > /etc/locale.conf'

一応vconsoleも設定します。

sudo sh -c 'echo KEYMAP=jp106 > /etc/vconsole.conf'

Hostname

ホスト名を適当に設定します。 /etc/hosts も忘れずに。

echo HOGEHOE > /etc/hostname
127.0.0.1    localhost.localdomain   localhost
::1     localhost.localdomain   localhost
127.0.1.1   HOGEHOGE.localdomain    HOGEHOGE

systemctl-boot

systemd-bootをブートローダーとしてインストールします。

bootctl --path=/boot install

rootfsを暗号化してるので、以下の /boot/loader/entries/arch.conf を作ります。

title Arch Linux Encrypted
linux /vmlinuz-linux
initrd /initramfs-linux.img
options cryptdevice=UUID=<UUID>:<mapped-name> root=UUID=<luks-UUID>  rootflags=subvol=@ quiet rw

UUIDは以下のコマンドで調べられます。

ls -l /dev/disk/by-uuid/

/dev/sda5、dm-0が<luks-UUID>なので、合わせて設定してください。

例えば

# ls -l /dev/disk/by-uuid/
lrwxrwxrwx 1 root root 10 Feb  8  2018 XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX -> ../../dm-0
lrwxrwxrwx 1 root root 10 Feb  8  2018 (hidden) -> ../../sdb2
lrwxrwxrwx 1 root root 10 Feb  8  2018 (hidden) -> ../../sdb1
lrwxrwxrwx 1 root root 10 Feb  8  2018 YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY -> ../../sda5
lrwxrwxrwx 1 root root 10 Feb  8  2018 (hidden) -> ../../sda2
lrwxrwxrwx 1 root root 10 Feb  8  2018 (hidden) -> ../../sda1
lrwxrwxrwx 1 root root 10 Feb  8  2018 (hidden) -> ../../sda7
lrwxrwxrwx 1 root root 10 Feb  8 02:01 (hidden) -> ../../sda6

となる場合は/boot/loader/entries/arch.conf は以下のようになります。

title Arch Linux Encrypted
linux /vmlinuz-linux
initrd /initramfs-linux.img
options cryptdevice=UUID=YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY:cryptroot root=UUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw rootflags=subvol=@ quiet

一緒に、Windowsをブートするためのエントリも作成してください。

title Windows 10
loader EFI/MICROSOFT/BOOT/BOOTMGFW.EFI

btrfs-progs

btrfs をつかうので、入れておきます。

sudo pacman -S btrfs-progs

これを入れておかないと、次のmkcpioでbtrfsのモジュールが入りません。

mkcpio

ブート時にrootfsの暗号化を解除できるよう /etc/mkinitcpio.conf に encrypt フックを追加します。

https://wiki.archlinux.jp/index.php/Dm-crypt/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E8%A8%AD%E5%AE%9A#encrypt_.E3.83.95.E3.83.83.E3.82.AF.E3.82.92.E4.BD.BF.E3.81.86

HOOKSの行を以下のように書き換えてください。

HOOKS=(base udev autodetect keyboard keymap consolefont modconf block encrypt btrfs filesystems)

initramfsを作り直します。

mkinitcpio -p linux

ansible動かせるように色々インストール

初期セットアップを別マシンからansibleで行えるようにしたいので、必要なファイルを入れておきます。

sudo pacman -S python openssh
sudo systemctl enable sshd.service

user追加

管理に使うユーザーを追加します。

useradd -m -G wheel USERNAME
passwd USERNAME

visudo

wheelのひとはNOPASSWDでsudo使えるようにしておくと、ansibleでの初期セットアップができて便利なので、設定しておきます。

リブート

最後にリブートして完了です。

exit
cd
umount -R /mnt
reboot

おしまい

今回はここでおしまいです。

QEMUのARM Virtマシンのペリフェラルメモリマップについてのメモ

QEMUのARM Virtマシンのペリフェラルメモリマップは何処を見ればいいんだっけ......とよく忘れるので、パスと見るべきところをメモします。

マシンを定義しているファイルはこれ。

qemu/virt.c at master · qemu/qemu · GitHub

メモリマップは125行目辺り MemMapEntry a15memmap[] にかかれています。 以下、該当コードを引用します。

static const MemMapEntry a15memmap[] = {
    /* Space up to 0x8000000 is reserved for a boot ROM */
    [VIRT_FLASH] =              {          0, 0x08000000 },
    [VIRT_CPUPERIPHS] =         { 0x08000000, 0x00020000 },
    /* GIC distributor and CPU interfaces sit inside the CPU peripheral space */
    [VIRT_GIC_DIST] =           { 0x08000000, 0x00010000 },
    [VIRT_GIC_CPU] =            { 0x08010000, 0x00010000 },
    [VIRT_GIC_V2M] =            { 0x08020000, 0x00001000 },
    /* The space in between here is reserved for GICv3 CPU/vCPU/HYP */
    [VIRT_GIC_ITS] =            { 0x08080000, 0x00020000 },
    /* This redistributor space allows up to 2*64kB*123 CPUs */
    [VIRT_GIC_REDIST] =         { 0x080A0000, 0x00F60000 },
    [VIRT_UART] =               { 0x09000000, 0x00001000 },
    [VIRT_RTC] =                { 0x09010000, 0x00001000 },
    [VIRT_FW_CFG] =             { 0x09020000, 0x00000018 },
    [VIRT_GPIO] =               { 0x09030000, 0x00001000 },
    [VIRT_SECURE_UART] =        { 0x09040000, 0x00001000 },
    [VIRT_MMIO] =               { 0x0a000000, 0x00000200 },
    /* ...repeating for a total of NUM_VIRTIO_TRANSPORTS, each of that size */
    [VIRT_PLATFORM_BUS] =       { 0x0c000000, 0x02000000 },
    [VIRT_SECURE_MEM] =         { 0x0e000000, 0x01000000 },
    [VIRT_PCIE_MMIO] =          { 0x10000000, 0x2eff0000 },
    [VIRT_PCIE_PIO] =           { 0x3eff0000, 0x00010000 },
    [VIRT_PCIE_ECAM] =          { 0x3f000000, 0x01000000 },
    [VIRT_MEM] =                { 0x40000000, RAMLIMIT_BYTES },
    /* Second PCIe window, 512GB wide at the 512GB boundary */
    [VIRT_PCIE_MMIO_HIGH] =   { 0x8000000000ULL, 0x8000000000ULL },
};

どのペリフェラルが使われているかはここからはわからないので、memmapの1列目の文字列から探していく必要があります。

例えばUARTなら、VIRT_UARTで検索をすると1422行目あたりに create_uart 関数を呼び出しているところが見つかります。

create_uart(vms, pic, VIRT_UART, sysmem, serial_hds[0]);

この create_uart 関数の定義は644行目あたりにあり、このコードを見ていくと頭の方にペリフェラルの名前が書いてあるので、PL011が使われていることがわかります。

static void create_uart(const VirtMachineState *vms, qemu_irq *pic, int uart,
                        MemoryRegion *mem, Chardev *chr)
{
    char *nodename;
    hwaddr base = vms->memmap[uart].base;
    hwaddr size = vms->memmap[uart].size;
    int irq = vms->irqmap[uart];
    const char compat[] = "arm,pl011\0arm,primecell";

後はペリフェラルの資料を探して読んでドライバを実装すればOKです。

おしまい。

UEFIのHTTP BOOTを試してみる

UEFI 2.5からネットワークブートの方法として"HTTP BOOT"が増えました。

今回はこのHTTP BOOTは従来の方式に比べ何が嬉しいのか、この機能を使ってUEFIのアプリを起動するにはどのようにすればよいかを書いていこうと思います。

忙しい人のまとめ

  • ネットワークブートがHTTP経由でできます
  • DHCPの設定変更なしでもOKです
  • 対応バージョンはUEFI 2.5以上です

HTTP BOOT とは

HTTP BOOTはUEFI 2.5で追加されたネットワークブートのためのUEFIの機能です。

この機能を使うと、HTTP(またはHTTPS)サーバーからファイルを取得し、UEFIアプリやOSを起動することができるようになります。

HTTP BOOT と PXEブートの違い

よく知られたネットワークブートの方法としてPXEブートがあります。これとHTTP BOOTはどのような点が異なるのでしょうか。 この疑問に関しては、以下のSUSE LinuxのドキュメントのIntroductionにわかりやすく書かれています。

UEFI HTTPBoot with OVMF - openSUSE

ここに書かれている内容をまとめると、HTTP BOOTではPXEに比べ以下の点が異なります。

  • HTTP URLを使って起動イメージ指定が可能
  • アドレス指定にDNSが利用可能
  • ローカルネットワーク外からもネットワークブート可能

また、UEFIの仕様書(24.7 HTTP Boot)やtinanocoreの解説サイトを読むと、以下の点でも異なることがわかります。

  • DHCPサーバーの設定変更が必須ではない(家庭用ルーターでもネットワークブート可能)
  • TFTPサーバー不要

このように、HTTP BOOTでは従来PXEによるネットワークブートを行うために必要であったDHCPサーバーの設定変更、TFTPサーバーの用意等が必要なくなるため、ネットワークブートのハードルが非常に低くなります。

実際にどのくらい簡単になるかを、想定環境の説明より確認していきましょう。

HTTP BOOTの想定環境

HTTP BOOTの想定環境には、以下の2つがあります。

  • Corporate Environment
  • Home Environment

それぞれどのような想定環境かを見ていきましょう。

Corporate Environment

最初にCorporate Environmentについて説明するために、UEFIの仕様書「24.7.2.1 Use in Corporate environment」のFigure 76より構成図を引用します。

f:id:tnishinaga:20171221030526p:plain

Corporate Environmentの想定は、基本的には従来のPXEブートを行える環境と同様です。

この環境ではDHCPサーバーに対しHTTP BOOT Extensionの設定を入れることで、クライアントの起動時にDHCPからアドレスと同時にHTTP BOOTのURL等をもらい、HTTP BOOTを使ったネットワークブートを開始することが可能です。

よって、Corporate Environmentでサーバー側で用意する必要が出るのは、最大で以下の3つになります。

  • "HTTP Extension設定可能"なDHCPサーバー(必須)
  • DNSサーバー(ドメイン名を使う場合は必要。IPアドレス指定の場合は不要)
  • HTTP(S)サーバー(外部の配布サイトを使う場合は不要)

Home Environment

次にHome Environmentについて説明するために、UEFIの仕様書「24.7.2.2 Use case in Home environment」のFigure 77より構成図を引用します。

f:id:tnishinaga:20171221032502p:plain

この環境はDHCPサーバーにHTTP BOOT Extensionの設定を入れることのできない、一般的な家庭用ルーターでHTTP BOOTを行うことを想定しています。 この環境では一般的なDHCPサーバーからIPアドレス(とDNSサーバー等の情報)のみを取得し、HTTP BOOTのURL等はユーザーが自分で入力することでHTTP BOOTを行います。

よって、Home Environmentでサーバー側で用意する必要が出るのは、最大で以下の3つになります。

  • 一般的なDHCPサーバー(必須)
  • DNSサーバー(ドメイン名を使う場合は必要。IPアドレス指定の場合は不要)
  • HTTP(S)サーバー(外部の配布サイトを使う場合は不要)

HTTP BOOTでUEFIアプリを動かしてみる

HTTP BOOTの使い方を確かめるために、Home Environmentにおいて実際にUEFIアプリをHTTP BOOTしてみようと思います。 なお、今回は動作確認が目的なので、OSの起動やインストール等まではやりません。

また、ターゲットはARM64環境とします。趣味です。

実験環境はいつもおなじみ Thinkpad x201と Arch Linux(x86_64) です。

HTTP BOOTに対応したUEFIのビルド

LinaroのビルドしてくれているQemuのイメージはHTTP BOOTが有効になっていなかったので、自分で有効にしてedk2からビルドし直します。

変更するところは edk2/ArmVirtPkg/ArmVirtQemu.dsc の以下の部分です。

  DEFINE HTTP_BOOT_ENABLE        = FALSE

ここのFALSEをTRUEにするとHTTP BOOTが有効になります。

!if $(HTTP_BOOT_ENABLE) == TRUE
  HttpLib|MdeModulePkg/Library/DxeHttpLib/DxeHttpLib.inf
!endif

残りのビルドの方法はansibleのスクリプトにしたので、このスクリプトを動かすか、中身を読んで同じことをコマンドで実行すればHTTP BOOTが有効になったQEMU用のUEFIファームウェア(ARM64用)が edk2/Build/ArmVirtQemu-AARCH64/RELEASE_GCC5/FV/QEMU_EFI.fd から得られます。

github.com

QEMU環境の用意

HTTP BOOTを行うには、QEMU仮想マシンを、ホストマシンが繋がっている家庭用のLANにつなげる必要があります。

このために、QEMUの作り出すTAPデバイスとホストマシンのNICをブリッジで繋ぐ必要があります。

この作業は理解するのが大変なので、勝手に行ってくれるスクリプトを作りました。

github.com

最初に、init.shIFACE="enp0s25" のenp0s25のところを任意の物理NIC名に変更して、init.shを実行してください。

sudo ./init.sh

これでホスト側にブリッジデバイスの作成とブリッジデバイスへのNICの追加が終わります。

次に、 run_qemu_aarch64.sh を実行すると、自動で qemu-ifupが実行され、QEMUの作ったTAPデバイスがブリッジに追加されます。

./run_qemu_aarch64.sh 

これで、LANにつながるQEMU仮想マシン環境が出来上がります。

HTTP BOOT

./run_qemu_aarch64.sh を実行してQEMU上でUEFI Shellが立ち上がったら、いよいよHTTP BOOTを行ってみます。

まずはじめに、IPアドレスの取得を行います。 これはUEFI Shellの上で以下のコマンドを打つと、簡単に設定できます。

ifconfig -s eth0 dhcp

UEFI Shell上ではpingコマンドが使えるので、ホストマシンのIPアドレスPingをうってみて、疎通を確認してください。 疎通が確認できれば、次に進みます。

HTTP BOOTはUEFI Shell上ではコマンドがなく行えないため、Shell上で exit コマンドを実行して一度マネージャーに戻ります。

f:id:tnishinaga:20171222221742p:plain

するとこのような画面が出てくるので、ここから Device Manager -> Network Device List -> NICのMACアドレスを選択 -> HTTP Boot Configuration と進みます。

f:id:tnishinaga:20171222012657p:plain

するとこのような画面が得られるので、 Boot URI のところに *.efi のあるURLを指定します。

今回はhttpboot/hello 以下でビルドしたmain.efiを以下のコマンドで起動したHTTPサーバーから配布しています。

python3 -m http.sever

ホストのIPアドレス192.168.111.50 でHTTPサーバーの待受ポートは8000番なので、http://192.168.111.50:8000/main.efiURIとして指定しています。

設定が終わったら ESCキー を押してください。 すると「設定を保存するならYをおしなさい」というメッセージが出るので、Yを押して保存します。 その後なんどかESCを押して、以下のマネージャーのTOPに戻ってください。

f:id:tnishinaga:20171222221742p:plain

次にBoot Manager を開くと一番最後に UEFI HTTP という項目が増えているはずなので、ここを選択してEnterを押します。

f:id:tnishinaga:20171222013348p:plain

しばらく待つとUEFIアプリが読み込まれて、私の作ったサンプルでは hello が表示されます。

f:id:tnishinaga:20171222014036p:plain

一つ注意として、HTTP BOOTはUEFIアプリの実行が終わるとすぐにブートマネージャーの画面に戻されてしまいます。 そうならないようしたい場合は、終了前にキー入力を待つような処理を最後に入れる必要がありそうです。

おわり

すみません、最近時間がなくて細かいところまで丁寧に書くことができませんでした。

質問をコメントでいただければ補足しますので、よろしくお願いします。