今回は要約関数について、この後で電子署名に言及する観点を持って述べます。
1. 要約関数のはたらき
テキストデータに対し要約関数を使ってダイジェストを生成するには下記のようにします。
<?php $data = 'This is a plain text to be signed.'; // 対象のメッセージと関数を指定し、OpenSSLの機能である「openssl_digest」でダイジェスト生成 $digest = openssl_digest($data, 'sha1'); // $digestは可読な文字列では無いのでbase64でエンコードして可読な文字列にする echo base64_encode($digest); ?>
上記の出力結果は「NzAwYWI1MzQ5ZDllMWM2ZGI1YWVlM2UxMDMzN2JhMzJkOGZiMWZkNw==」です。
ちなみに関数をMD5にした場合は「ZTNmZDZkMWNkYWYyZDk4ODUyYmViOWI4ZmYzMWJiZjQ=」というように、用いる関数(ハッシュ関数/ダイジェスト・メソッド)によって得られるダイジェストが異なりますので注意が必要です。
2. ダイジェストの特性と使用について
ダイジェストを生成する対象のメッセージにスペースが1つ追加されただけでもダイジェストの値は劇的に変わります。上記の例の「This is a plain text to be signed.」の末尾にあるピリオドを取り去って、「This is a plain text to be signed」に対するダイジェストを生成してみると下記のようになります。
<?php $data = 'This is a plain text to be signed'; // 対象のメッセージと関数を指定し、OpenSSLの機能である「openssl_digest」でダイジェスト生成 $digest = openssl_digest($data, 'sha1'); // $digestは可読な文字列では無いのでbase64でエンコードして可読な文字列にする echo base64_encode($digest); ?>
生成されたダイジェストは「ZDdjYmE1NzhkY2U3MmYxZDdhN2JiYzQ0YWRkZjA4ZWU0NTQ2ZGEwMw==」です。メッセージの末尾のピリオドを取り去っただけで全く異なるダイジェストが生成さてることがわかります。
本記事はは要約関数を使ってダイジェストを生成し後にそのダイジェストを含む電子署名を生成するという前提でいるわけですが、電子署名でメッセージの改竄を判別できるようにする際には何を持って「異なる」とするのかの基準が必要です。別の言い方をすれば下記のAとBのメッセージが同一か異なるかについての基準です。
A: This is a pen.
B: This is a pen.
上記は人間にとって受け取る情報は同一ですが、ダイジェスト生成に関しては一般的には異なるメッセージとみなします。これは、人間が個別に読んで理解することが前提であるメッセージではデータとしての相違は厳密に扱うが、読んだ結果同じ意味であるかどうかは人間の判断に委ねられるからです。
しかしながら、XMLのようなシステムが読んで解釈する場合はコンピュータにとっての意味が同一であるにも関わらず些末な書き方の違いで異なるとみなすのはあまりにも非合理的です。自動処理の利便性を損ないます。なので、XMLのようなコンピュータが解釈する前提のデータはダイジェスト生成の前に「正規化」という処理をXMLに対して行います。
3. XMLのダイジェスト生成
3.1. XML正規化
上で少し言及したように、XMLのダイジェストを生成する際には要約関数にかける前にXMLを「正規化(Canonicalization)」するようにW3Cの勧告にて規定されています。規定の内容はとてもわかりにくい(と個人的には思う)ので、要点を書くとXMLに対して下記の処理を行います。
- XML宣言があれば取り除く
- DOCTYPE宣言があれば取り除く
- コメントは取り除く
- XMLタグと同レベルにある改行コードは取り除く
- 全角スペースは半角スペースに置き換える
- 連続するスペースは1文字の半角スペースに置き換える
- 要素値や属性値に含まれる改行コードは半角スペース1文字に置き換える
- XMLタグ内の先頭や末尾あるスペースは取り除く
- 要素値や属性値に含まれるCR(復帰), LF(改行), TAB(タブ)は半角スペースに置き換える
- 要素値や属性値に含まれる先頭と末尾の空白は取り除く
- なお、XML内で属性値を囲うクォートはダブルクォートに統一
- 空要素は<tagname/>ではなく、<tagname></tagname>と記述する
ダブルクォートかシングルクォートかについてはXMLとしてはどちらを用いてもwell-formedであり問題ないのですが、PHPでXMLをパースすると自動的にダブルクォートに変換されます。ですので、弊社としては「XML内で属性値を囲うクォートはダブルクォートに統一」に統一します。
これを充足するようにXMLを整形するPHPのコードのサンプルが下記です。実際にめちゃくちゃにスペースやらコメントやら改行やらが入った下記のXMLを実際に整形してみます。
<?php $xmlstr = <<<cXML xml:lang='en-US' payloadID='933694607739' timestamp='2002-08-15T08:46:00-07:00'> <!-- no header required for response --> < Response> <Status code='200' text='success'></Status> <PunchOutSetupResponse> <StartPage> <URL> http://xml.workchairs.com/retrieve?reqUrl=20626;Initial=TRUE</URL> <Empty/> <Status code="200" text="OK"/> <Status code="200" text="OK"/> </StartPage> </PunchOutSetupResponse> </Response> </cXML> XML; //タブ・改行を半角スペースに変換 $xmlstr = preg_replace('/(\t|\r\n|\r|\n)/s', ' ', $xmlstr); //全角スペースを半角スペースに置換 $xmlstr = preg_replace('/ /', ' ', $xmlstr); //連続する半角スペースを1つの半角スペースにまとめる $xmlstr = preg_replace('/\s+/', ' ', $xmlstr); //コメントを削除 $xmlstr = preg_replace('//s', '', $xmlstr); //XML宣言を削除 $xmlstr = preg_replace('/<\?xml[\s\S]*?\?>/s', '', $xmlstr); //DOCTYPE宣言を削除 $xmlstr = preg_replace('//s', '', $xmlstr); //XMLタグ外 //タグの閉じカッコと開きカッコの間にスペースしかない場合はスペースを削除する $xmlstr = preg_replace('/>\s', '><', $xmlstr); //XMLタグ外にある閉じカッコ直後のスペースは削除 $xmlstr = preg_replace('/>\s/', '>', $xmlstr); //XMLタグ外にある開きカッコの直前のスペースは削除 $xmlstr = preg_replace('/\s', '<', $xmlstr); //XMLタグ内 //XMLタグ内にある閉じカッコ直後のスペースは削除 $xmlstr = preg_replace('/\s>/', '>', $xmlstr); //XMLタグ内にある開きカッコの直前のスペースは削除 $xmlstr = preg_replace('/<\s/', '<', $xmlstr); //attribute値の先頭のスペースを削除 $xmlstr = preg_replace('/="\s/', '="', $xmlstr); $xmlstr = preg_replace("/='\s/", "='", $xmlstr); //attribute値の末尾のスペースを削除 $xmlstr = preg_replace('/="(\S+)\s"/', '="$1"', $xmlstr); $xmlstr = preg_replace("/='(\S+)\s'/", "='$1'", $xmlstr); //空要素のタグを自己閉じタグ(self-closing tag)ではなくするにはDOMとして扱い「LIBXML_NOEMPTYTAG」を使う $dom = new DOMDocument("1.0"); $dom->loadXML($xmlstr); $canonicalized = $dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG); echo $canonicalized; // 対象のメッセージと関数を指定し、OpenSSLの機能である「openssl_digest」でダイジェスト生成 $digest = openssl_digest($canonicalized, 'sha1'); // $digestは可読な文字列では無いのでbase64でエンコードして可読な文字列にする echo base64_encode($digest); ?>
という処理です。処理結果として生成された文字列($xmlstr)は下記の通りです。ちゃんと正規化されています。
<cXML xml:lang="en-US" payloadID="933694607739" timestamp="2002-08-15T08:46:00-07:00"><Response><Status code="200" text="success"></Status><PunchOutSetupResponse><StartPage><URL>http://xml.workchairs.com/retrieve?reqUrl=20626;Initial=TRUE</URL><Empty></Empty><Status code="200" text="OK"></Status><Status code="200" text="OK"></Status></StartPage></PunchOutSetupResponse></Response></cXML>
3.2. XMLのダイジェストの生成
上記で正規化したXMLのダイジェストをSHA1で生成すると下記の値が得られます。
NmQ2ZjFjOGZhNTIwMjcyMTlhYWFlMDI4OTQ2NzJjZjEyMTUyNjU5Yw==
次回はやっと本筋である電子署名についてです。