魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

Python の hmac モジュールのメモ

API キーの検証ロジックやらを実装しようとするとたまに登場するであろう hmac モジュールです。

RFC レベルまで理解しようとすると苦行だと思いますが、このモジュール自体の基本的なところはあっさりしているのでまとめました。

docs.python.org

公式ドキュメントがしっかりしているから特に存在意義はないと思います。

new メソッドでオブジェクト生成

new メソッドで HMAC オブジェクトを生成します。 第一引数はハッシュ化するための key 、第二引数はハッシュ化する文字列、 第三引数がハッシュ化するためのアルゴリズムです。

>>> import hashlib, hmac
>>> key = b'foo'
>>> msg = b'test'
>>> a = hmac.new(key, msg, hashlib.sha256)

ダイジェスト値の取得

digest メソッドでバイト列のダイジェスト値が取得できます。ダイジェスト値ってなんだ?

>>> a.digest()
b':\\\x147aB\x83\xa4\xc5Wg\x0f1\x96\xc5m4\xdb\xc5\x10\x9f+\xf3\xdbs\xe61\xcb\x13p\xa4\xe2'

hexdigest メソッドを使うと16進数の形式で取得できます。API キーなどで使いたい場合はこちらの方が便利かもしれません。

>>> a.hexdigest()
'3a5c1437614283a4c557670f3196c56d34dbc5109f2bf3db73e631cb1370a4e2'

ちなみに binascii モジュール を使うと digest の結果と hexdigest の結果を相互変換できます。

  • digest → hexdigest
>>> import binascii
>>> d = a.digest()
>>> binascii.hexlify(d).decode('utf-8')
'3a5c1437614283a4c557670f3196c56d34dbc5109f2bf3db73e631cb1370a4e2'
  • hexdigest → digest
>>> h = a.hexdigest()
>>> binascii.unhexlify(h)
b':\\\x147aB\x83\xa4\xc5Wg\x0f1\x96\xc5m4\xdb\xc5\x10\x9f+\xf3\xdbs\xe61\xcb\x13p\xa4\xe2'

docs.python.org

ダイジェスト値の検証

このハッシュ(のダイジェスト値)ですが、クライアントから送信されてきた値とサーバー側で計算した値が等しいかどうかを検証する処理がセットになることが多いと思います。

そんなときは compare_digest メソッドを使います。

>>> a_byte = hmac.new(key, b'hoge', hashlib.sha256).digest()
>>> b_byte = hmac.new(key, b'hoge', hashlib.sha256).digest()
>>> hmac.compare_digest(a_byte, b_byte)
True

>>> c_byte = hmac.new(key, b'fuga', hashlib.sha256).digest()
>>> hmac.compare_digest(a_byte, c_byte)
False

digest の戻り値( byte 型)でも hexdigest の戻り値( str 型)でも OK です。

>>> a_hex = hmac.new(key, b'hoge', hashlib.sha256).hexdigest()
>>> b_hex = hmac.new(key, b'hoge', hashlib.sha256).hexdigest()
>>> hmac.compare_digest(a_hex, b_hex)
True

ただしどちらかに統一されている必要があり、digest の戻り値と hexdigest の戻り値を直接比較しようとしても TypeError になります。

>>> hmac.compare_digest(a_byte, a_hex)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: a bytes-like object is required, not 'str'

ダイジェスト値の取得の他の方法

new メソッドを経由せずに直接ダイジェスト値(バイト列)を得ることができます。 先ほど何回か登場した digest メソッドは new メソッドで生成した HMAC オブジェクトに対するインスタンスメソッドですが、こちらはクラスメソッドです。

>>> hmac.digest(key, msg, hashlib.sha256)
b':\\\x147aB\x83\xa4\xc5Wg\x0f1\x96\xc5m4\xdb\xc5\x10\x9f+\xf3\xdbs\xe61\xcb\x13p\xa4\xe2'

ちなみに、クラスメソッドとしての hexdigest はないです。

>>> hmac.hexdigest(key, msg, hashlib.sha256)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'hmac' has no attribute 'hexdigest'

update メソッドの話

冒頭紹介したnew メソッドですが、第二引数の「ハッシュ化する文字列 msg 」は必須ではありません。

>>> a = hmac.new(key, None, hashlib.sha256)
# あるいはこう
>>> a = hmac.new(key, digestmod=hashlib.sha256)

そして、この HMAC オブジェクトに対して、update メソッドを使うと msg を後から追加することができます。

>>> a.update(b'hoge')

使い方はこんな感じです。

>>> a = hmac.new(key, None, hashlib.sha256)
>>> b_digest = hmac.digest(key, b'hoge', hashlib.sha256) # 比較用
>>> hmac.compare_digest(a.digest(), b_digest)
False # 当然違う値
>>> a.update(b'hoge')
>>> hmac.compare_digest(a.digest(), b_digest)
True # 同じ

オブジェクトを直接更新する点に注意が必要です。

>>> a.update(b'hoge')
>>> hmac.compare_digest(a.digest(), b_digest)
False
>>> c = hmac.digest(key, b'hogehoge', hashlib.sha256) # 比較用
>>> hmac.compare_digest(a.digest(), c)
True

ちなみに new メソッドではなく digest メソッド HMAC オブジェクトを生成する場合は msg を None にすることはできません。

>>> hmac.digest(key, None, hashlib.sha256)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/kazuma/.anyenv/envs/pyenv/versions/3.9.5/lib/python3.9/hmac.py", line 201, in digest
    inner.update(msg)
TypeError: object supporting the buffer API required

new メソッドで生成したあとに digest メソッドを使った場合は何の問題もないんですけどね。まぁ困るわけではないので別に良いのですが…

>>> hmac.new(key, None, hashlib.sha256).digest()
b'h7\x16\xd9\xd7\xf8.\xed\x17Ll\xae\xbe\x08n\xe93v\xc7\x9d|a\xddg\x0e\xa0\x0f\x7f\x8dn\xb0\xa8'

あとがき

Python の言語仕様の説明に終始してしまいましたが、仕組み自体をもう少し深いところまで理解したいのでそのための準備運動ということで一旦この辺で…