hmac – 暗号化シグネチャとメッセージ検証

目的:RFC 2104 で提案されたメッセージ認証の鍵付きハッシュを提供する
利用できるバージョン:2.2

HMAC アルゴリズムは、潜在的に脆弱性のあるところで格納されるデータ、もしくはアプリケーション間で受け渡しする情報の整合性検証に使用されます。基本的な考え方は、共有秘密鍵を組み合わせた実際のデータの暗号化ハッシュを生成することです。それにより生成されたハッシュは、秘密鍵を転送することなく、信頼性を決定する格納メッセージや通信そのものをチェックするために使用されます。

免責事項: 私はセキュリティの専門家ではありません。HMAC の詳細は RFC 2104 を調べてください。

サンプル

ハッシュを作成するのは難しくありません。デフォルトの MD5 ハッシュアルゴリズムを使用する簡単なサンプルは次になります。

import hmac

digest_maker = hmac.new('secret-shared-key-goes-here')

f = open('lorem.txt', 'rb')
try:
    while True:
        block = f.read(1024)
        if not block:
            break
        digest_maker.update(block)
finally:
    f.close()

digest = digest_maker.hexdigest()
print digest

実行すると、このスクリプトはソースファイルを読み込み HMAC シグネチャを算出します。

$ python hmac_simple.py
4bcb287e284f8c21e87e14ba2dc40b16

Note

今週サンプルソースをリリースするときに変更がなければ、ダウンロードしたサンプルソースも同じハッシュ値を生成します。

SHA と MD5

hmac のデフォルトの暗号化アルゴリズムは MD5 ですが、MD5 は最もセキュアなメソッドではありません。MD5 ハッシュは、衝突(2つの異なるメッセージが同じハッシュを生成する)といった脆弱性があります。SHA-1 アルゴリズムはより強力と考えられており、こちらを使用すべきです。

import hmac
import hashlib

digest_maker = hmac.new('secret-shared-key-goes-here', '', hashlib.sha1)

f = open('hmac_sha.py', 'rb')
try:
    while True:
        block = f.read(1024)
        if not block:
            break
        digest_maker.update(block)
finally:
    f.close()

digest = digest_maker.hexdigest()
print digest

hmac.new() は3つの引数を取ります。1番目の引数は、同じ値で通信する両方の終端で共有される秘密鍵です。2番目の引数は、初期化メッセージです。認証が必要なメッセージの本文が、タイムスタンプや HTTP POST といった小さなデータの場合、全てのメッセージ本文は update() ではなく new() へ渡されます。3番目の引数は、使用するダイジェストモジュールです。デフォルトは hashlib.md5() です。このサンプルは hashlib.sha1() に置き換えています。

$ python hmac_sha.py
69b26d1731a0a5f0fc7a92fc6c540823ec210759

バイナリダイジェスト

前説のサンプルは、表示可能なダイジェストを生成するために hexdigest() メソッドを使用します。hexdigest は digest() メソッドが算出する異なる値の表現です。それは NUL も含めた表示不可能な文字、または ASCII ではない文字を含む可能性のあるバイナリ値です。(Google chekckout や Amazon S3 といった) web サービスによっては、hexdigest ではなく、バイナリダイジェストを base64 でエンコードしたものを使用します。

import base64
import hmac
import hashlib

f = open('lorem.txt', 'rb')
try:
    body = f.read()
finally:
    f.close()

digest = hmac.new('secret-shared-key-goes-here', body, hashlib.sha1).digest()
print base64.encodestring(digest)

base64 エンコード文字列は改行文字で終わりを表します。HTTP ヘッダ、またはその他のフォーマットに注意が必要なコンテンツに base64 エンコード文字列を組み込むときは改行を取り除く必要があります。

$ python hmac_base64.py
olW2DoXHGJEKGU0aE9fOwSVE/o4=

アプリケーション

HMAC 認証は、任意の公開ネットワークサービスでセキュリティが要求されるデータに使用すべきです。例えば、パイプやソケットを通してデータを送信するとき、そのデータを署名してから送信して、そのデータを使用する前にそのシグネチャを検証すべきです。次の拡張したサンプルは、PyMOTW ソースパッケージにある hmac_pickle.py ファイルで利用できます。

まず文字列のダイジェストを算出する関数を定義して、シンプルなクラスをインスタンス化して通信チャンネルを通して渡します。

import hashlib
import hmac
try:
    import cPickle as pickle
except:
    import pickle
import pprint
from StringIO import StringIO


def make_digest(message):
    "Return a digest for the message."
    return hmac.new('secret-shared-key-goes-here', message, hashlib.sha1).hexdigest()


class SimpleObject(object):
    "A very simple class to demonstrate checking digests before unpickling."
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name

次にソケットかパイプを模倣する StringIO バッファを作成します。普通に使用しますが、データストリームのフォーマットや解析は簡単です。ダイジェストとデータ長を書き込んだ後で改行します。 pickle で生成されたオブジェクトのシリアライズ表現は次の通りです。実際のシステムでは、ダイジェストが不正な場合はおそらくデータも不正になるので、データ長の値は不要かもしれません。より適切な現実のデータには、何らかの終了シーケンスが現れない可能性が高いです。

このサンプルでは、ストリームに2つのオブジェクトを書き込みます。1番目のオブジェクトは正しいダイジェスト値を使用して書き込みます。

# StringIO で書き込みソケットかパイプを模倣する
out_s = StringIO()

# ストリームへ正常なオブジェクトを書き込む
#  digest\nlength\npickle
o = SimpleObject('digest matches')
pickled_data = pickle.dumps(o)
digest = make_digest(pickled_data)
header = '%s %s' % (digest, len(pickled_data))
print '\nWRITING:', header
out_s.write(header + '\n')
out_s.write(pickled_data)

2番目のオブジェクトは、pickle 化されたデータではなく、別のデータのダイジェストを算出して生成した不正なダイジェストでストリームへ書き込みます。

# ストリームへ不正なオブジェクトを書き込む
o = SimpleObject('digest does not match')
pickled_data = pickle.dumps(o)
digest = make_digest('not the pickled data at all')
header = '%s %s' % (digest, len(pickled_data))
print '\nWRITING:', header
out_s.write(header + '\n')
out_s.write(pickled_data)

out_s.flush()

いま、書き込んだデータは StringIO バッファにあり、そのデータを読み返します。まずはダイジェストとデータ長の行を読み込みます。それから、残りのデータを(データ長の値を利用して)読み込みます。ストリームから直接読み込むのに pickle.load() を使用できますが、その処理は信頼できるデータストリームを前提としているので、信頼できないデータに対してはまだ unpickle しません。文字列としての pickle を読み込むには、オブジェクトを実際に unpickle せずにストリームからデータを読み込みます。

# StringIO で読み込みソケットかパイプを模倣する
in_s = StringIO(out_s.getvalue())

# データを読み込む
while True:
    first_line = in_s.readline()
    if not first_line:
        break
    incoming_digest, incoming_length = first_line.split(' ')
    incoming_length = int(incoming_length)
    print '\nREAD:', incoming_digest, incoming_length
    incoming_pickled_data = in_s.read(incoming_length)

pickle 化されたデータを見つけたら、読み込んだデータに対してダイジェスト値を再計算して比較します。算出したダイジェストが一致したら、読み込んだデータが信頼して良いデータだと分かるので、そのデータを unpickle します。

actual_digest = make_digest(incoming_pickled_data)
print 'ACTUAL:', actual_digest

if incoming_digest != actual_digest:
    print 'WARNING: Data corruption'
else:
    obj = pickle.loads(incoming_pickled_data)
    print 'OK:', obj

その実行結果は、1番目のオブジェクトは検証され、2番目のオブジェクトは期待した通り “Data corruption(データ破損)” と見なされます。

$ python hmac_pickle.py

WRITING: 387632cfa3d18cd19bdfe72b61ac395dfcdc87c9 124

WRITING: b01b209e28d7e053408ebe23b90fe5c33bc6a0ec 131

READ: 387632cfa3d18cd19bdfe72b61ac395dfcdc87c9 124
ACTUAL: 387632cfa3d18cd19bdfe72b61ac395dfcdc87c9
OK: digest matches

READ: b01b209e28d7e053408ebe23b90fe5c33bc6a0ec 131
ACTUAL: dec53ca1ad3f4b657dd81d514f17f735628b6828
WARNING: Data corruption

See also

hmac
本モジュールの標準ライブラリドキュメント
RFC 2104
HMAC: メッセージ認証のキーハッシュ
hashlib
hashlib モジュール
pickle
シリアライズライブラリ
WikiPedia: MD5
MD5 ハッシュアルゴリズムの説明
Amazon S3 Web サービスの認証
HMAC-SHA1 署名で S3 の認証を行う方法
Bookmark and Share