暗号署名

ウェブアプリケーションのセキュリティの黄金律は、信頼できない情報源からのデータを決して信頼しないことです。時には、信頼されていない媒体を通してデータを渡すことが有用な場合もあります。暗号的に署名された値は、改ざんが検知されることを前提に、信頼されていない経路を安全に通過させることができます。

Django は、値の署名に関する低レベル API と、Web アプリケーションでの署名の最も一般的な用途の一つである署名付きクッキーの設定と読み取りに関する高レベル API の両方を提供しています。

また、以下のような場合にも署名が役に立つでしょう:

  • パスワードを紛失したユーザーに送信する「アカウントの回復」URLの生成。
  • 非表示のフォームフィールドに保存されたデータが改ざんされていないことの確認。
  • 保護されたリソースへの一時的なアクセスを許可するためのワンタイムシークレットURLの生成。例えばユーザーが料金を支払ったダウンロード可能なファイルなど。

SECRET_KEYSECRET_KEY_FALLBACKS の保護

startproject によって新しいプロジェクトを作る際、 settings.py が生成され、ランダムな SECRET_KEY を得ます。この値が、署名されたデータを安全に保つ鍵になります。このキーを安全に保存するとこは極めて重要で、さもなければ攻撃を行うひとたちが彼らの署名を生成するのにも使えるからです。

SECRET_KEY_FALLBACKS は秘密鍵のローテーションに使用できます。この値はデータへの署名には使用されませんが、指定された場合は署名されたデータを検証するために使用されるので、安全に保つ必要があります。

低レベルの API を利用する

Django の署名メソッドは django.core.signing モジュールにあります。値に署名するには、まず Signer インスタンスを作成します。

>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign("My string")
>>> value
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'

署名が文字列の最後にコロンに続いて付加されます。元の値を取得するには、unsign メソッドを使用します。

>>> original = signer.unsign(value)
>>> original
'My string'

もし sign に文字列でない値を渡すと、その値は署名される前に強制的に文字列に変換され、unsign の結果にはその文字列の値が返されます。

>>> signed = signer.sign(2.5)
>>> original = signer.unsign(signed)
>>> original
'2.5'

リスト、タプル、辞書を保護したい場合は 、sign_object()unsign_object() メソッドを使います。

>>> signed_obj = signer.sign_object({"message": "Hello!"})
>>> signed_obj
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
>>> obj = signer.unsign_object(signed_obj)
>>> obj
{'message': 'Hello!'}

詳細は 複雑なデータ構造の保護 を参照してください。

何らかの理由により署名の値が書き換わってしまった場合、django.core.signing.BadSignature 例外が起こります。

>>> from django.core import signing
>>> value += "m"
>>> try:
...     original = signer.unsign(value)
... except signing.BadSignature:
...     print("Tampering detected!")
...

デフォルトでは、Signer クラスは SECRET_KEY 設定の値を署名生成に利用します。次のように Signer のコンストラクタに任意の値を渡すことで、異なる秘密鍵を使用することもできます。

>>> signer = Signer(key="my-other-secret")
>>> value = signer.sign("My string")
>>> value
'My string:EkfQJafvGyiofrdGnuthdxImIJw'
class Signer(*, key=None, sep=':', salt=None, algorithm=None, fallback_keys=None)[ソース]

署名の生成に key を使用し、値の区切りに sep を使用する signer (署名器)を返します。 sep には URL セーフな base64 英数字 を使用することはできません。このアルファベットには英数字、ハイフン、アンダースコアが含まれます。 algorithmhashlib がサポートするアルゴリズムでなければなりません。デフォルトは 'sha256' です。fallback_keys は署名されたデータを検証するために使用する追加の値のリストです。デフォルトは SECRET_KEY_FALLBACKS です。

salt 引数を使用する

特定の文字列が出現するたびに同じ署名ハッシュを使用したくない場合は、オプションで Signer クラスの引数 salt を使用できます。ソルトを使用すると、署名ハッシュ関数にソルトと SECRET_KEY の両方をシードにします:

>>> signer = Signer()
>>> signer.sign("My string")
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
>>> signer = Signer(salt="extra")
>>> signer.sign("My string")
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
>>> signer.unsign("My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw")
'My string'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
>>> signer.unsign_object(
...     "eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I"
... )
{'message': 'Hello!'}

この方法でソルトを使用すると、異なる署名が異なる名前空間に配置されます。 ある名前空間(特定のソルト値)に由来する署名は、異なるソルト設定を使用している別の名前空間で、同じ平文文字列を検証するためには使用できません。その結果、攻撃者がコードのある場所で生成された署名付き文字列を、異なるソルトを使用して署名を生成(検証)している別のコードの入力として使うことを防止できます。

SECRET_KEY とは異なり、salt 引数は秘密にする必要はありません。

タイムスタンプ値の検証

TimestampSignerSigner のサブクラスで、値に署名されたタイムスタンプを追加します。これにより、署名された値が指定された期間内に作成されたことを確認できます:

>>> from datetime import timedelta
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign("hello")
>>> value
'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
>>> signer.unsign(value)
'hello'
>>> signer.unsign(value, max_age=10)
SignatureExpired: Signature age 15.5289158821 > 10 seconds
>>> signer.unsign(value, max_age=20)
'hello'
>>> signer.unsign(value, max_age=timedelta(seconds=20))
'hello'
class TimestampSigner(*, key=None, sep=':', salt=None, algorithm='sha256')[ソース]
sign(value)[ソース]

value に署名し、現在のタイムスタンプを追加します。

unsign(value, max_age=None)[ソース]

値が max_age 秒前より後に署名されたことを確認します。パラメータ max_age には整数または datetime.timedelta オブジェクトを指定します。

sign_object(obj, serializer=JSONSerializer, compress=False)

複雑なデータ構造(例えば、リスト、タプル、または辞書)をエンコードし、オプションで圧縮し、現在のタイムスタンプを追加し署名します。

unsign_object(signed_obj, serializer=JSONSerializer, max_age=None)

値が signed_obj 秒前より後に署名されたことを確認します。パラメータ max_age には整数または datetime.timedelta オブジェクトを指定します。

複雑なデータ構造の保護

リスト、タプル、辞書を保護したい場合は、 Signer.sign_object()unsign_object() メソッド、あるいは署名モジュールの dumps()loads() 関数 (TimestampSigner(salt='django.core.signing').sign_object()/unsign_object() のショートカットです) を使って保護できます。これらは JSON シリアライズを使っています。JSONにすることで、 SECRET_KEY が盗まれても、攻撃者が pickle フォーマットを悪用して任意のコマンドを実行できないようにします:

>>> from django.core import signing
>>> signer = signing.TimestampSigner()
>>> value = signer.sign_object({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1kx6R3:D4qGKiptAqo5QW9iv4eNLc6xl4RwiFfes6oOcYhkYnc'
>>> signer.unsign_object(value)
{'foo': 'bar'}
>>> value = signing.dumps({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1kx6Rf:LBB39RQmME-SRvilheUe5EmPYRbuDBgQp2tCAi7KGLk'
>>> signing.loads(value)
{'foo': 'bar'}

JSONの性質上(ネイティブではリストとタプルの区別がありません)、タプルを渡すと signing.loads(object) からリストが返されます:

>>> from django.core import signing
>>> value = signing.dumps(("a", "b", "c"))
>>> signing.loads(value)
['a', 'b', 'c']
dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False)[ソース]

URL セーフな、署名付き base64 圧縮 JSON 文字列を返します。シリアライズされたオブジェクトは TimestampSigner を使って署名されます。

loads(string, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None, fallback_keys=None)[ソース]

dumps() の逆で、署名に失敗した場合に BadSignature を返します。指定された場合は max_age (秒単位) をチェックします。

Back to Top