Ubuntu 12.04 (Precise) のインストールではまったこと

普通にインストールするぶんにははまらないけど個人的事情ではまった部分です。
つまり備忘録。

LVM を有効にしたインストール

基本路線は Ubuntu 8.04 を LVM 有効にしてインストールする - daily dayflower のとおり。ただし,dm-mod はインストールする必要ない (だいぶ前からだけど)。
一点はまったのは,「インストールされた環境に lvm2 をインストールする」部分。
ネットワークブートな Live 環境からインストールしようとしたからかもしれないけど,chroot したあとにうまくネットにつながらなかった。

procfs をマウントしたり,Live 環境の /etc/resolve.conf をコピーする必要があった。

ubuntu@ubuntu:~$ sudo mount /dev/sda1 /target

ubuntu@ubuntu:~$ sudo mount -o bind /proc /target/proc

ubuntu@ubuntu:~$ sudo cp /etc/resolve.conf /target/etc/

ネットブートからのインストールだと,ここで /target/etc/network/interface をいじっといたほうがいいかも (eth0 な部分を削除するか dhcp に指定する)。

あとは元の手順通り。

ubuntu@ubuntu:~$ sudo chroot /target

root@ubuntu:/# apt-get update

root@ubuntu:/# apt-get install lvm2

root@ubuntu:/# exit

終了時は umount もよしなに。

ubuntu@ubuntu:~$ sudo umount /target/proc

ubuntu@ubuntu:~$ sudo umount /target

winbind 認証ログイン

基本的に winbind による Active Directory 認証 on Ubuntu 11.04 - daily dayflower のとおりなんだけど,Samba が 3.6.3 になってて設定ディレクティブ等がかわってたのではまった。

[global]
        workgroup = HOGE
        realm = HOGE.EXAMPLE.COM
        security = ADS
        obey pam restrictions = Yes

        algorithmic rid base = 10000
        template homedir = /home/%U
        template shell = /bin/bash

#       winbind separator = !
        winbind cache time = 60
        winbind use default domain = Yes
        winbind nss info = sfu:HOGE
        winbind refresh tickets = Yes
        winbind normalize names = Yes
        winbind enum users = Yes
        winbind enum groups = Yes

        idmap config * : backend = tdb
        idmap config * : range = 1000000-1999999
        idmap config DOMAIN : backend = rid
        idmap config DOMAIN : range = 10000-19999

idmap のデフォルトの backend 等は idmap config * : のように,ドメインごとの設定と統一感のある記述になった (古い書き方でも deprecated っていわれるだけだけど)。
あと,じっさいに使うドメイン向けじゃなくてデフォルトのを tdb idmapper 等の永続性のある backend に指定しておく必要があるみたい (BUILTIN sid とかのために((idmap_rid(8) のマニュアルのうけうり。)) )。この部分に一番はまった。

winbind ユーザも lightdm からログインする

ディスプレイマネージャが lightdm に変更されていて,こいつはデフォルトだと登録されたユーザしかログオンできない (ユーザ名は選択する)。このままだと,winbind や ldap なユーザはログインできない。
ユーザ名も自分で入力できるようにするためには lightdm の設定をいじる必要がある。
具体的には,/etc/lightdm/lightdm.conf[SeatDefaults] セクションに下記のような設定を付け足す。

[SeatDefaults]
greeter-show-manual-login=true

DataMapper でカスタムリレーション

DataMapper - Associations の「Customizing Associations」に書いてある。


User と Message というモデルがあり,Message には宛先が(複数)指定できる,とする。できるだけ規約ベースで書くと以下のようになる。

class User
  include DataMapper::Resource

  property :id, Serial

  has n, :message_receipients
  has n, :messages,
    :through => :message_receipients
end

class Message
  include DataMapper::Resource

  property :id, Serial

  has n, :message_receipients
  has n, :users,
    :through => :message_receipients
end

class MessageReceipient
  include DataMapper::Resource

  property :id, Serial

  belongs_to :user
  belongs_to :message
end

u = User.create
m = Message.new
m.users << u
m.save

p m.users
p u.messages

でもこれだと,「宛先」ぽさとか User の「受信したメッセージ」ぽさの意図がなくなってわかりにくい。

class User
  has n, :message_receipients,
    :child_key => [ :receipient_id ]
  has n, :received_messages, :model => 'Message',
    :through => :message_receipients,
    :via => :message
end

class Message
  has n, :message_receipients
  has n, :receipients, :model => 'User',
    :through => :message_receipients
end

class MessageReceipient
  belongs_to :receipient, :model => 'User'
  belongs_to :message
end

u = User.create
m = Message.new
m.receipients << u
m.save

p m.receipients
p u.received_messages

要するに :child_key とか (今回は使ってないけど) :parent_key とか :via などのオプションを使えば,リレーション時にどのような外部キーを使うかなどをカスタマイズできる。

新規開発だけでなくレガシースキーマを相手にする場合にも。

まぁ ORM でリレーションまでやってしまうかどうかという問題もあるけど。

Microsoft Word で LL 言語から差し込み文書作成

タイトルは釣り。
簡単な帳票 (Excel 系じゃなくて Word 系の文書) をプログラムから作成する必要がでてきたので調べた。テンプレートとなる文書にちょっとした変数の差し込みを行う必要があるケース。
ただし

  • 旧来の Microsoft Office Document (OLE2) はサポートしない
  • COM や UNO は使わない
  • (ODF や OOXML の) 仕様書は読まない

LibreOffice (OpenOffice.org) や最近の Microsoft Office だと,XML ファイルを ZIP で固めているだけなので単純な差し込みだけならわりと簡単にできそう。
注意: ODF にしても OOXML にしても XML namespace を使っているので,厳密なことをいうと以下の例のようにタグ名を決め打ちで置換していてはうまくいかないことがありうる。とはいえそうそう変更しないだろうからこのままでうまくいくだろうし,気になるなら対象タグの xmlns の部分だけバリデーション (というかサポートするターゲットかどうか判別) すればいいんじゃないかと思う。

OpenDocument Format (ODF) 編

LibreOffice 3.3.2 Writer on Ubuntu 11.04 で作成したドキュメントで検証をおこなった。
内容は「こんにちは ○○ さん」というだけの単純なもの。○○の部分にプログラムから差しこみたい。

テンプレート文書の作成

差しこみフィールドの作成のやり方。

  1. 「挿入」メニューから「フィールド」「その他」メニューを選択する
  2. 「機能」タブを選択
  3. 「フィールドタイプ」で「プレースホルダ」を選択,「書式」は「テキスト」を選択
  4. プレースホルダ」に何らかのプレースホルダ名 (テンプレート変数的なもの) をいれる
  5. 「挿入」ボタンを押下
展開
$ unzip ../hello.odt

Archive:  ../hello.odt
 extracting: mimetype                
  inflating: content.xml             
  inflating: manifest.rdf            
  inflating: styles.xml              
 extracting: meta.xml                
 extracting: Thumbnails/thumbnail.png  
  inflating: Configurations2/accelerator/current.xml  
   creating: Configurations2/progressbar/
   creating: Configurations2/floater/
   creating: Configurations2/popupmenu/
   creating: Configurations2/toolpanel/
   creating: Configurations2/menubar/
   creating: Configurations2/toolbar/
   creating: Configurations2/images/Bitmaps/
   creating: Configurations2/statusbar/
  inflating: settings.xml            
  inflating: META-INF/manifest.xml   

無駄に空フォルダが多いが,実態は追える範囲。

解析と置換

文書の内容はおもに content.xml に記述されている。
今回の主眼となる部分の抜粋 (改行やインデントは編集してある; 元は全部1行*1 )。

<text:p text:style-name="Standard">
  こんにちは
  <text:placeholder text:placeholder-type="text">&lt;hogehoge&gt;</text:placeholder>
  さん!
</text:p>

単純明快。
Perlワンライナーでざっくり置換してみた。XML を展開・再構築するほうが行儀いいんだろうけど,バリデーションを行うのでもなければ PCRE で必要十分な気がする。

$ perl -i -pe 's{<text:placeholder .*?>
                 \s* &lt;hogehoge&gt; \s*
                 </text:placeholder>      }{dayflower}gxms' content.xml

これで一応完成。だけど,文書の統計情報 (何文字含まれているかとか何ページあるかとか) も文書に含まれている。基本的に文書ファイルのプロパティで表示するためだけのものなので間違っていても構わないんだけど,なんか気持ち悪いので削除しておく。
統計情報の実態は meta.xml というファイルの <meta:document-statistic/> タグに存在する。以下は例。

<meta:document-statistic
   meta:table-count="0"
   meta:image-count="0"
   meta:object-count="0"
   meta:page-count="1"
   meta:paragraph-count="1"
   meta:word-count="3"
   meta:character-count="20"/>

なので meta.xml ファイルから該当する XML タグを削除しておけばよい。

再構築

普通に再圧縮するだけ。

$ zip -r9 ../new.odt *

updating: Configurations2/ (stored 0%)
updating: META-INF/ (stored 0%)
......

作成された文書を再度 LibreOffice で開いてみたところ,きちんと埋め込みされていた。

Office Open XML (OOXML) 編

Microsoft Office 2010 Word で作成したドキュメントで検証をおこなった。

テンプレート文書の作成

差しこみフィールドの作成のやり方。

  1. 「挿入」メニュータブにおいて「クイックパーツ」を選択し「フィールド」メニューを選択
  2. 「フィールドの選択」で「差し込み印刷」フィルタを選び*2,「フィールドの名前」から「MergeField」を選択
  3. 「フィールドプロパティ」の「フィールド名」に適宜名前 (テンプレート変数的なもの) をつける
展開
$ unzip ../hello.docx

Archive:  ../hello.docx
  inflating: [Content_Types].xml     
  inflating: _rels/.rels             
  inflating: word/_rels/document.xml.rels  
  inflating: word/document.xml       
  inflating: word/theme/theme1.xml   
  inflating: word/settings.xml       
  inflating: word/webSettings.xml    
  inflating: word/stylesWithEffects.xml  
  inflating: docProps/core.xml       
  inflating: word/styles.xml         
  inflating: word/fontTable.xml      
  inflating: docProps/app.xml        

無駄な空フォルダは少ない。結構多くのファイルに分散しているのは ODF と同等。

解析と置換

文書の内容はおもに word/document.xml に記載されている。
今回のターゲットとなる部位の抜粋。

<w:p w:rsidR="00332438" w:rsidRDefault="00792A30">
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>こんにちは</w:t>
  </w:r>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t xml:space="preserve"> </w:t>
  </w:r>
  <w:fldSimple w:instr=" MERGEFIELD  hogehoge  \* MERGEFORMAT ">
    <w:r>
      <w:rPr>
        <w:noProof/>
      </w:rPr>
      <w:t>&#171;hogehoge&#187;</w:t>
    </w:r>
  </w:fldSimple>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t xml:space="preserve"> </w:t>
  </w:r>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>さん!</w:t>
  </w:r>
  <w:bookmarkStart w:id="0" w:name="_GoBack"/>
  <w:bookmarkEnd w:id="0"/>
</w:p>

埋め込みを行う立場から見ると,ちょっとめんどい (あくまで ODF に比べての話だけど)。MergeField じゃないもっと適切なフィールドタイプがあるのかもしれない。

フィールド埋め込みについては Perlワンライナーで書くなら以下のような感じ。

$ perl -pe 's{<w:fldSimple .*?>
                (.*? <w:t>)
                  .*?
                (</w:t> .*?)
              </w:fldSimple>    }{$1dayflower$2}gxms' document.xml

さきほどの ODF の例とは置換内容がちょっと違う (全埋め込みフィールド対象になっちゃってる) けど,ちゃんとやるなら置換後のほうにコード仕込んでとかやるかな。
OOXML でもやはり文書の統計情報が存在する。実態は docProps/app.xml のいくつかのタグ。以下抜粋。

<Pages>1</Pages>
<Words>9</Words>
<Characters>52</Characters>
<Lines>1</Lines>
<Paragraphs>1</Paragraphs>
再構築
$ zip -r9 ../new2.docx *
  adding: [Content_Types].xml (deflated 75%)
  adding: _rels/ (stored 0%)
......

作成された文書を再度 Microsoft Word 2010 で開いてみたところ,きちんと埋め込みされていた。

同じ値を複数ヶ所に同時に埋め込みたい場合

もちろん全プレースホルダを上記のようにプログラムから書き換えてもいいんだけど,もっとスマートな解決策もある。OpenDocument などにはユーザ定義変数のようなものがあって,それを文書中に埋め込むことができる。この変数の値の部分だけ書き換えれば,文書中のそのユーザ変数フィールドは全部置き換わることになる。
以下は OpenDocument での例。面倒なので Microsoft Word ではどうやるか調べていないが,同等の機能はたぶんあると思う。

  1. 「挿入」メニューから「フィールド」「その他」メニューを選択する
  2. 「変数」タブを選択
  3. 「フィールドタイプ」で「ユーザー欄」を選択
  4. 「名前」フィールドに「変数名」を,「値」フィールドにデフォルトの値を入力
  5. (書式は「テキスト」を選択しておくのが無難)

これでユーザ定義変数が定義される (「選択」ペインにユーザ変数一覧が表示される)。
あとは文書中の挿入したい場所にカーソルを移動して「フィールド」の「挿入」ダイアログをさきほどと同じように出し,「選択」ペインで作成したユーザ定義変数を選択し,「挿入」ボタンを押下すれば埋め込まれる。
このときの content.xml の内容は以下の通り。

<text:user-field-decls>
  <text:user-field-decl office:value-type="string" office:string-value="hogehoge" text:name="holder"/>
</text:user-field-decls>
<text:p text:style-name="Standard">
  こんにちは
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get>
  さん
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get>
  さん
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get> さん!
</text:p>

さきほどと差しこみフィールドの実現のされ方が結構違っている。
変数部分 (<text:user-field-decl>) のほうだけ書き換えればいいので,適当なワンライナーだけど (ほんとうなら text:name のほうでチェックするべき)

$ perl -i -pe 's{string-value="hogehoge"}{string-value="dayflower"}gxms' content.xml

こんな感じで。
実態のドキュメント部分 (<text:user-field-get>) のほうは置き換えてないけど,LibreOffice で再度開いた際には変数の埋めこまれた部分は自動的に変更されていた。ファイル自体はちょっと気持ち悪い状態だけど。

メモ

その他既存のアプローチについて (COM 以外)。

  • Perl の場合
    • OpenOffice-OODoc を使えば OpenDocument を読み・書き・操作できるみたい?そこそこメンテナンスされているようだ。
  • Ruby の場合
    • OOo4R という OpenDocumento を読み書きするプロジェクトがあるようだが 2004 年あたりで停滞しているようだ。
    • roo というプロジェクトがあるが表計算系 (OpenDocument ods, Excel xls, xlsx, Google Spreadsheet) の読み込みしかできないようだ。
    • UFOOAR というプロジェクトがあり Ruby 用の UNO ブリッジを目指しているようだ。2008 年あたりで停滞か?
  • Java の場合
    • そもそも UNO を使えば Java から OpenOffice.org, LibreOffice 等を操れるはず。
    • Apache POI で OLE2 (旧来の MS Office document コンテナ) や OOXML のファイルを読み・書き・操作できるようだ。
      • とはいえコアはあくまでコンテナの読み書きっぽい。各アプリケーション (Word など) の文書を操作する高次 API も用意されているようだが (例: Apache POI HWPF) クオリティについては不明。

*1:このため vim で編集しようとしたら半ばフリーズ気味になってしまった。

*2:もちろんフィルタリングせず全フィールドタイプから選択してもいい。

winbind による Active Directory 認証 on Ubuntu 11.04

Ubuntu での winbind による Active Directory 認証は以前 winbind で Linux 認証 on Ubuntu - daily dayflower で書いたんだけど,だいぶ古くなったんで今の環境むけに。一応,対象 Natty だけど Maverick とかたぶん Lucid あたりまで適用できると思う。

面倒といえば面倒なのでそれを回避したいなら LikeWise 使うとか (第174回 Likewise OpenでActive Directoryを利用する:Ubuntu Weekly Recipe|gihyo.jp … 技術評論社 参照)。ただ LikeWise もカスタマイズされた Samba 等を内包してたりするんで,中身が見えないと嫌な人には向いてない。

諸元

Linux マシン
Active Directory サーバ

2011年にもなるのに Windows 2000 Server なんてセキュリティの面とかいろいろ間違っている気がするけど,大人の事情。Windows Server 2003 以降なら SFU schema はいらない。というか 2000 Server でもシェル等が決め打ちでよかったりすれば SFU schema はいらない。

凡例

以前のを踏襲。

ドメイン
HOGE
Active Directory のレルム
hoge.example.com
Active Directoryドメインコントローラ
dc.hoge.example.com
クライアントホスト名
Penguin

Kerberos の設定

デフォルトでは入っていないんで krb5-config をインストール。

$ sudo apt-get install krb5-config

以前は krb5-user もインストールしていたんだけど,今は kinit しなくても net ads join できるんでインストールする必要はないと思う。実際なくてもできた。

/etc/krb5.conf の抜粋:

[libdefaults]
	default_realm = HOGE.EXAMPLE.COM

[realms]
	HOGE.EXAMPLE.COM = {
		kdc = dc.hoge.example.com
		admin_server = dc.hoge.example.com
		default_domain = hoge.example.com
	}

[domain_realm]
	.hoge.example.com = HOGE.EXAMPLE.COM
	hoge.example.com = HOGE.EXAMPLE.COM

Samba, winbind の設定

samba, winbind をインストール。

$ sudo apt-get install samba winbind

/etc/samba/smb.conf の抜粋:

[global]
	workgroup = HOGE
	realm = HOGE.EXAMPLE.COM
	security = ADS
	obey pam restrictions = Yes

	algorithmic rid base = 10000
	template homedir = /home/%U
	template shell = /bin/bash

#	winbind separator = !
	winbind cache time = 60
	winbind use default domain = Yes
	winbind nss info = sfu:HOGE
	winbind refresh tickets = Yes
	winbind normalize names = Yes
	winbind enum users = Yes
	winbind enum groups = Yes

	idmap uid = 10000-19999
	idmap gid = 10000-19999
	idmap backend = rid

winbind separator を指定するとなぜか pam モジュールでの認証がうまくいかない。winbind separator を指定した場合というか winbind normalize names と併用した場合の気がする。なので泣く泣く (?) 指定をはずしている。ま,実際には使うことほとんどないし,DOMAIN\user な指定はメジャー (かつ Windows 側との整合性もとれてる) んで泣く泣くというほどではない。ただ,いざ使うときはシェル等から使うことが多いからエスケープが必要だったりしてダサい。

Active Directory への参加とテスト

さきほど書いたとおり kinit は必要ないんでいきなり net ads join する。

$ sudo net ads join -U Administrator
Administrator's password: ********
Using short domain name -- HOGE
Joined 'PENGUIN' to realm 'hoge.example.com'
No DNS domain configured for penguin. Unabled to perform DNS Update.
DNS update failed!

以前書いたとおり,手元の環境では DNSドメインコントローラに任せていないんで,上記のように怒られている。これでも問題なく使える。

きちんと join できてるかテスト((testjoin は join の試行を行うのではなく,join がされているかどうか調べるコマンド。名前が悪い。))。

$ sudo net ads testjoin
Join is OK

できてる。

いよいよ winbind を立ち上げる。

$ sudo service winbind start
 * Starting the Winbind daemon winbind
   ...done.

なぜか winbind だけは upstart 形式になってない。ので SysV init 系の service コマンドで起動 (もちろん /etc/init.d/winbind で実行してもいいけど)。

きちんと winbind が働いているか調べる。

$ wbinfo -t
checking the trust secret for domain HOGE via RPC calls failed
Could not check secret

$ sudo wbinfo -t
checking the trust secret for domain HOGE via RPC calls succeeded

一瞬焦ったが,wbinfo -t に限っては root 権限でないと実行できないようだ。

Active Directory のグループ一覧を取得してみる (こちらはユーザ権限で可; ユーザ一覧は -u オプション)。

$ wbinfo -g
netshow administrators
dhcp users
dhcp administrators
...... (以下略)

nss の設定

winbind が起動しただけでは,Active Directory のユーザ情報は winbind 内で完結しており Linux から使えない。これを Linux のユーザ情報管理システムと結合するのが nss である。

/etc/nsswitch.conf の抜粋:

passwd:         compat winbind
group:          compat winbind
shadow:         compat winbind

shadow: んとこはいらない気もするけど一応。RHEL ではコマンドで設定できたけど,Ubuntu だと全部手書きしなくちゃなんない。

ついでに,Windows マシンの名前解決ができるように hostswins をいれておく。

hosts:          files mdns4_minimal [NOTFOUND=return] dns wins mdns4

うまくユーザ情報を利用できるかテスト。

$ id dayflower
uid=11190(dayflower) gid=10513(domain_users) 所属グループ=10513(domain_users),10512(domain_admins)

できてる。もちろん getent passwd とかでたしかめてもよい。

pam の設定

以上で Active Directory 上のユーザのアカウント (情報) が Linux のアカウントと結合された。だが,ログイン時等の認証は pam 経由でおこなっているのでそちらの設定をする必要がある。

設定に必要なファイルはインストールされているので pam-auth-update を実行するだけでよい。

$ sudo pam-auth-update

のだが,ホームディレクトリが存在していないユーザはログイン等できない。これでは不便なので,認証時にホームディレクトリが存在しない場合,自動的にホームディレクトリを作成するようにする。

ホームディレクトリの作成自体は pam_mkhomedir.so を使えばできる。これをさきほどの pam-auth-update コマンドで導入されるよう,下記のようなファイルを作成する。

/usr/share/pam-configs/mkhomedir :

Name: Automatic User Dir Generation
Default: no
Priority: 192
Session-Type: Additional
Session:
	optional	pam_mkhomedir.so skel=/etc/skel/ umask=0022

実は (いつごろからか忘れたけど) pam_winbind.somkhomedir というオプションが用意され,それを使えばわざわざ pam_mkhomedir.so を利用する必要はなくなった。んでもそっちだと umask= 等のオプションはないし,システム提供の /usr/share/pam-configs/winbind を書き換える必要があるので敬遠してる。


pam の認証とは関係ないが,Domain Admins に所属するユーザは sudo できたほうがいいので,そのように設定する。

$ sudo visudo

/etc/sudoers の抜粋:

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL
%domain_admins ALL=(ALL) ALL

winbind normalize names = Yes という設定にしているので domain_admins という指定になっている。普通に書くなら domain\ admins

あと,このままだと GNOME 等で音がでなかったり USB 機器が使えなかったりする可能性がある*1ので,/etc/group を編集して,ログインするユーザを各グループに適宜所属させている。人数が多くなると面倒 (だし一元管理の趣旨に反する) ので,その場合は /etc/security/group.conf を設定すればうまくできるのかな?ただ現状 (Natty 11.04) だと pam_grouplogin にしか記述されてないんでうまくいくのか不明。

起動スクリプト (upstart)

以上で Active Directory に登録されているユーザであればこの Linux マシンにログインできる。

そのためには winbind さえあがっていればよい。Windows マシンからこのホスト名 (例では Penguin) を名前解決するためには nmbd もあがっていたほうがよい。SMB 共有を行わないのであれば smbd はあがっている必要はない。

ということで起動時に smbd をあがらないようにしてみる (Ubuntu だと Samba をインストールすると smbd, nmbd が自動的に起動するようになっているので)。smbd, nmbd は upstart 経由でサービスコトロールされているので,下記のファイルをいじる (のだと思う。 upstart にくわしくないのでほんとうにこれでいいかわからない)。

/etc/init/smbd.conf の抜粋:

#start on (local-filesystems and net-device-up)

start on というところをコメントアウトした。

あとは今現在あがっている smbd をおとす。

$ sudo initctl stop smbd

もちろん sudo stop smbd でもよい。

*1:惰性で /etc/group いじってしまってるんで,もしいじらなかった場合に使えないのかどうか実は知らない。

Java で RSA 暗号を使う

まず OpenSSL コマンドで RSA 秘密鍵を生成する。

$ openssl genrsa -out private_key.pem 1024

Generating RSA private key, 1024 bit long modulus
........................++++++
...++++++
e is 65537 (0x10001)

末尾にビット数を指定している。OpenSSL のコマンド側だとたとえば 256 とかでも通るんだけど,これを Java のほうにもってくと

java.security.InvalidKeyException: RSA keys must be at least 512 bits long

のように怒られたりする。ただし Sun JRE 付属のプロバイダ (JCE) だとそうなだけで Bouncy Castle をプロバイダに使うと怒られなかった。

このあと openssl rsa コマンドで DER 形式にすればいいだけかなと思ったらのちのち怒られたので PKCS #8 形式かつ DER 形式に変換する。

openssl pkcs8 -in private_key.pem -topk8 -nocrypt -outform DER -out private_key.pk8

公開鍵はふつうに DER 形式で出力すればいいだけ。

openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der
writing RSA key


以上のようにして OpenSSL で生成した鍵を使って Java で暗号化・復号化してみる。

なお,本来公開鍵暗号方式では,平文を公開鍵 (受信者側が公開した鍵) で暗号化し,暗号文を秘密鍵 (受信者側が秘匿している鍵) で復号化する。だが,今回の案件では秘密鍵 (送信者側が秘匿している鍵) で暗号化し,公開鍵 (送信者側が公開した鍵) で復号をおこなった。つまり RSA による暗号化というより半ば署名的に使ったことになる。通信内容の秘匿には使えないが,

  • 第3者に暗号文を生成させたくない
  • カジュアルに解読されなければいい

などの理由によりこのような仕様とした。

プログラムは以下のとおり。

package com.example.rsajava;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.Cipher;

import org.apache.commons.codec.binary.Base64;

public class Main {
	private static final String CIPHER_ALGORITHM = "RSA";
	private static final String CIPHER_MODE = CIPHER_ALGORITHM + "/ECB/PKCS1PADDING";

	private static final String PRIVATE_KEY_FILE = "private_key.pk8";
	private static final String PUBLIC_KEY_FILE  = "public_key.der";

	public static void main(String[] args) {
		Main app = new Main();

		InputStreamReader input = new InputStreamReader(System.in);
		BufferedReader reader = new BufferedReader(input, 1);
		for (;;) {
			System.out.print("INPUT:     ");
			System.out.flush();

			String line;
			try {
				line = reader.readLine();
				if (line == null) break;
			} catch (IOException e) {
				e.printStackTrace();
				break;
			}

			byte[] encrypted = app.encryptWithPrivateKey(line.getBytes());
			System.out.print("ENCRYPTED: ");
			System.out.println(Base64.encodeBase64String(encrypted));

			byte[] decrypted = app.decryptWithPublicKey(encrypted);
			System.out.print("DECRYPTED: ");
			System.out.println(new String(decrypted));

			System.out.println();
		}
	}

	private Cipher cipher;
	private Key secret_key;
	private Key public_key;

	public Main() {
		try {
			cipher = Cipher.getInstance(CIPHER_MODE);


			KeyFactory keyfactory = KeyFactory.getInstance(CIPHER_ALGORITHM);
			KeySpec keyspec;

			keyspec = new PKCS8EncodedKeySpec(loadBinaryFile(PRIVATE_KEY_FILE));
			secret_key = keyfactory.generatePrivate(keyspec);

			keyspec = new X509EncodedKeySpec(loadBinaryFile(PUBLIC_KEY_FILE));
			public_key = keyfactory.generatePublic(keyspec);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	public byte[] encryptWithPrivateKey(byte[] source) {
		try {
			cipher.init(Cipher.ENCRYPT_MODE, secret_key);
			return cipher.doFinal(source);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	public byte[] decryptWithPublicKey(byte[] source) {
		try {
			cipher.init(Cipher.DECRYPT_MODE, public_key);
			return cipher.doFinal(source);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	private static byte[] loadBinaryFile(String filename) {
		try {
			FileInputStream in = new FileInputStream(filename);
			byte[] data = new byte[in.available()];
			in.read(data);
			in.close();
			return data;
		} catch (IOException e) {
			e.printStackTrace();
		}

		return new byte[0];
	}
}

かなり RSA 暗号のコード - BiBoLoG からパクっている。Base64 形式の出力のため Apache Commons Codec を利用している。あと例外処理は適当すぎる。


以下実行例。

INPUT:     Hello, World!
ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL
Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB
JvlpobhKF9DLZGJ3JJ0=

DECRYPTED: Hello, World!


INPUT:     Hello, World!
ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL
Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB
JvlpobhKF9DLZGJ3JJ0=

DECRYPTED: Hello, World!


INPUT:     hello, World!
ENCRYPTED: NR7vRLEkRHLpUwFzmgS2lf8FwkpUiX0HGnv6rCgx9wOc2SFupXVog3+7AJmGZlq7RA24LrRKCjsS
awIwtiyPaPDVtT+MYgmb5DWcV9fivewygvw5Ct6qRmvjUMl26+uglYkQ0yF6g250rp8NOJa1k5l/
0NBx4e7ls5Q2C7drHjw=

DECRYPTED: hello, World!

(少なくとも Java での) RSA 暗号化には初期ベクタがないので,同じ文から同じ暗号文が生成されている (1 つめの例と 2 つめの例)。しかし 3 つめの例から少なくとも先頭の文字が違っていれば暗号文の全体 (というかブロック長) が変化するので,自分で初期化ベクタを用意してやればカジュアルに傍受している側をまどわせることができると思われる。

ほか注意点

  • ブロック長 (= 鍵の長さ) で自動的に分割して暗号化してくれるわけではないので自分で分割する必要がある
    • といっても一般に通常鍵の長さをこえるほどの情報はやりとりしないであろう。共通鍵暗号の鍵を最初にやりとりするだけであれば,RSA 鍵長内におさまるわけだし
  • (すくなくとも PKCS#1) パディングをおこなうと,パディングのための領域が必要になる。1024 bit の鍵の場合に暗号化できるブロック内平文は 1024 bit に満たない

KVM で PXE ブートしようとしたら難儀した

KVM の場合,virt-install コマンドに --pxe オプションをつければ PXE ブートさせることができる。Xen と違って完全仮想化だから。ばんざい。

# virt-install --name=hogehoge --accelerate --os-variant=rhel5.4 \
 --ram=384 --vcpus=1            \
 --file=hogehoge.img            \
 --network=bridge:br0 --pxe     \
 --vnc --noautoconsole

あるいは qemu-kvm コマンドを直接叩いて network option rom を指定するという方法もあるようだ (参考, qemu-kvm の直接起動は Stray Penguin - Linux Memo (KVM-2) 参照)。

これで普通に DHCP サーバと TFTP サーバを構成していればうまく PXE ブートできるはずなのだが,手元の環境では以前 Proxy DHCP サーバで PXE ブート - daily dayflower で書いたように Proxy DHCP を使っている。

この Proxy DHCP 環境下で KVMPXE ブートさせようとしたらなかなかうまくいかなかった。


結論からいうと,KVM (Qemu) で利用しているネットワークコントローラ用の ROM (Etherboot) にバグがあった。

ちなみに Etherboot 由来のネットワークブート ROM は Oracle VirtualBox でも使っている。ので同様に Proxy DHCP 環境ではうまく PXE できない (かった)。

原因 1: Proxy DHCPOFFER の NBP file name に対応していない

Proxy DHCP サーバで PXE ブート - daily dayflower でも書いたように,PXE_DISCOVERY_CONTROL Option の bit 3 を立てると DHCPOFFER の段階でブートファイル名を返すことができる。

ところが,Etherboot 由来の PXE BIOS はこのオプションを無視する。そして UDP 4011 宛に DHCPREQUEST を投げてくる。

しかたないので pxe-pdhcp を改造して 68 番と 4011 番両ポートで待ち受けするように変更した。

原因 2: Etherboot の ProxyDHCP 時の DHCPREQUEST パケットがおかしい

これで一件落着かと思ったら,DHCPREQUEST のベンダオプション項がうまくパースできない。仕様の読み漏れかと思って Etherboot のサイトからソースを落として読んだら,バグがあった。ベンダオプション項の最後に End of Options (FFh) をつけ忘れている。

Etherboot 4.4.2 でのパッチは以下のとおり。

diff -r e53c2fc7ea6a src/core/nic.c
--- a/src/core/nic.c    Wed Feb 16 12:19:14 2011 +0900
+++ b/src/core/nic.c    Wed Feb 16 14:07:59 2011 +0900
@@ -1166,6 +1166,7 @@
                                /* Construct the ProxyDHCPREQUEST packet */
                                memcpy(ip.bp.bp_vend, rfc1533_cookie, sizeof rfc1533_cookie);
                                memcpy(ip.bp.bp_vend + sizeof rfc1533_cookie, proxydhcprequest, sizeof proxydhcprequest);
+                               ip.bp.bp_vend[sizeof rfc1533_cookie + sizeof proxydhcprequest] = RFC1533_END;
                                for (reqretry = 0; reqretry < MAX_BOOTP_RETRIES; ) {
                                        printf ( "\nSending ProxyDHCP request to %@...", arptable[ARP_PROXYDHCP].ipaddr.s_addr);
                                        udp_transmit(arptable[ARP_PROXYDHCP].ipaddr.s_addr, BOOTP_CLIENT, PROXYDHCP_SERVER,

報告しようと思ったらバグトラッカも落ちてるしレポジトリもみつからない。どうもサイトが故障していたようだ。一部サービス (ソース提供や rom-o-matic での NIC ROM 自動生成) はすでに復旧している。

一応上記のパッチを適用してビルドした ROM ではうまく PXE boot できた((NIC ROM の置き換えは,/usr/share/kvm 以下の pxe-virtio.bin などを置き換える。もともとは /usr/share/qemu-pxe-roms/ 以下の ROM ファイルへのシンボリックリンク。))。だが,前述したように,VirtualBox でも問題のある Etherboot 由来の NIC ROM を使っており,こちらをどのように差し替えるかがわからない。なので,pxe-pdhcp 側で workaround をおこなった。

ほんとは workaround のソースを示せればいいんだけど,諸般の事情*1でまだだせない。あくまで Etherboot のバグ対策だけだが,

  • DHCP Options の Class Identifier が 32 バイト続いたあとで,(本来 FFh があるべきだが) 「:UNDI:〜」とゴミが続いてしまっている
  • よって pdhcp.ccheck_dhcp_packet() において DHCP Option が 3Ah, 55h の並びになった場合,そこを終端とみなすようにする

という対策をおこなったところ,CentOS 5.5 の KVM guest および VirtualBox 4.0.2 において PXE ブートでできるようになった。

余談

Etherboot プロジェクトは実質 gPXE プロジェクトが後継となっているようだ。こちらのソースは一から書きおこしている部分が多く,上記のようなバグはない (なさそうだった)。

だが gPXE の NIC ROM を生成して KVM で使ってみたところ,なぜか主 DHCP サーバ宛に TFTP 要求をだしていた。ソース上は ProxyDHCP 用のコードもあるようだがまだきちんとインプリメントされていないのかもしれない。

なお,Etherboot では上記のパッチを適用して自分で ROM をビルドしたが,gPXE の NIC ROM は http://rom-o-matic.net/gpxe/gpxe-1.0.1/contrib/rom-o-matic/NIC ROM を生成した。いろいろ条件を指定して NIC ROM を生成できるのでおもしろいサイトである。


ちなみに,Etherboot (と gPXE) では,条件を指定することで TFTP だけでなく,HTTP 経由でイメージをとってこれる NIC ROM も作成できるようだ。また,gPXE のほうは iSCSI による SAN Boot にも対応しているらしい (id:adsaria さんによる実践例)。

*1:諸般の事情と書くとおおげさだけど,単に面倒だから。id:viver さんが pxe-pdhcp のソースを github にうつしてくれればいいなぁ。

Debian 6.0 Live イメージを PXE 経由でブート

さいきんの DebianLive 起動用イメージも用意しているらしい。

んで,Live 起動イメージはネットワークブート用のイメージも用意されている。

たとえば,http://ftp.jaist.ac.jp/pub/Linux/Debian-CD/6.0.0-live/i386/net/ をみるといくつかの種類 (GNOME 環境とか Rescue 環境とか) のネットブート用 Live イメージ (*.tar.gz) がおいてあることがわかる。

そこで,CentOS で作った PXE ブート環境に,選択肢のひとつとして Debian 6.0 Live を追加してみた。index.rb [nofuture.tv] が参考になる。

なお PXE 環境自体の構築については今回触れない。


ライブイメージディストリビューションのなかから debian-live-6.0.0-i386-standard.tar.gz を選択してみた。standard といいつつほとんど何も入ってないプレーンな状態だけど。

取得して展開すると,だいたい下記のようなディレクトリツリーになってる。

+ debian-live/
+ tftpboot/
  + tftpboot/debian-live/
  + tftpboot/debian-install/

おおまかにいうと,tftpboot/ フォルダ以下が TFTP による初期ブートに必要なファイル群 (カーネルとか initramfs とか)。debian-live/ は initramfs が起動したあとに NFS 経由で読み込むルートイメージ。NFS 経由といっても生ルートツリーを NFS マウントするわけじゃなくてルートイメージが SquashFS 形式でおいてあるんでそれを NFS 経由で読み込んでマウントする感じ。

なお tftpboot/debian-install/ のほうは,Debian をネットワークインストールする際に使う TFTP ブートイメージで,今回は使用しなかった。

NFS 用ルートイメージの用意

前述のとおり debian-live/ 以下が NFS 経由のルートイメージになるので適当な場所において NFS で公開する。

# cp -R debian-live /pub/linux/debian/squeeze

別にディレクトリはこの名前にする必要があるわけじゃない。自分の環境では上記のようにしているだけ。


上記例だと exports はこんな感じ。

# cat /etc/exports

/pub/linux/debian *(ro,all_squash,no_subtree_check,crossmnt)
/pub/linux/debian/squeeze *(ro,all_squash,no_subtree_check,crossmnt)

んで NFS デーモンを再起動。

# service nfs restart

PXE用ファイルの用意

こんどは TFTP ブート用ファイルを /tftpboot/ 以下の適当なディレクトリにコピー。

# cp -R tftpboot/debian-live /tftpboot/debian-squeeze

んで,Debian のドキュメントとかだと付属する pxelinux.cfg をそのまま使うような形で紹介されてるんだけど,そうするといままで使用していた他の PXE ブート環境がなくなってしまってもったいないので要所だけコピーする。

Debian Live の場合,たとえば debian-live/i386/boot-screens/live.cfgPXE ブート用の設定が書いてある。

label live
        menu label Live
        kernel debian-live/i386/vmlinuz-2.6.32-5-486
        append initrd=debian-live/i386/initrd.img-2.6.32-5-486 boot=live
               config netboot=nfs nfsroot=192.168.1.1:/srv/debian-live quiet

改行いれちゃってるけど,append 行のとこは1行で書く必要があると思う (以下も同様)。これをもとに,自分の pxelinux.cfg に書き足す。


今回の例示だと以下のような内容。/tftpboot/ の下とか NFSディレクトリに好き勝手な名前をつけたので,対応する部分を変更してある。

LABEL squeezelive
  MENU LABEL Debian 6.0 Live
  KERNEL debian-squeeze/i386/vmlinuz-2.6.32-5-486
  APPEND initrd=debian-squeeze/i386/initrd.img-2.6.32-5-486 boot=live config
         netboot=nfs nfsroot=<NFS Server Address>:/pub/linux/debian/squeeze quiet


以上で無事 Debian 6.0 Squeeze の Live 環境が立ち上がった。あまりにプレーンなのでほんとにうまく動いてるか自信がないので今 GNOME 版の Live イメージをダウンロード中*1


なお,Ubuntu でも同じようにすれば Live 環境の PXE 起動はできる。つっても違うところはいろいろあって

  • TFTP ブート用のファイルは netboot.tar.gz というのをとってくる
  • NFS 用ルートイメージは,Ubuntu インストール DVD をマウントしたものを NFS エクスポートすればそのまま使える
  • pxelinux.cfg まわりは preseed とか casper まわりとかを APPEND 行でいろいろ指定する必要がある

いろいろたいへんそうだけど,たいていの人は Ubuntu のライブイメージを手元に持ってるだろうから,ハードルは Debian より高くない。

Ubuntu の場合の pxelinux.cfg 例を下記においとく。

LABEL mavericklive
  MENU LABEL Ubuntu 10.10 Live
  KERNEL ubuntu-maverick/i386/casper/vmlinuz
  APPEND initrd=ubuntu-maverick/i386/casper/initrd.lz
         file=ubuntu-maverick/i386/preseed/ubuntu.seed
         boot=casper netboot=nfs
         nfsroot=<NFS Server Address>:/pub/linux/ubuntu/maverick quiet splash --

TFTP 用ファイルは /tftpboot/ubuntu-maverick/ においており,NFS 用ルートイメージが /pub/linux/ubuntu/maverick/ においてあるという前提だけど。当然ながら KERNEL 行や APPEND 行のファイルパスは適宜書き換えること。

*1:GNOME 版できちんと GNOME があがったので手順として間違ってないことを確認した。しかしいまや Debian も Live 環境からインストールできるのね。怖くてためしてないけど。