暗号署名¶
ウェブアプリケーションのセキュリティの黄金律は、信頼できない情報源からのデータを決して信頼しないことです。時には、信頼されていない媒体を通してデータを渡すことが有用な場合もあります。暗号的に署名された値は、改ざんが検知されることを前提に、信頼されていない経路を安全に通過させることができます。
Django は、値の署名に関する低レベル API と、Web アプリケーションでの署名の最も一般的な用途の一つである署名付きクッキーの設定と読み取りに関する高レベル API の両方を提供しています。
また、以下のような場合にも署名が役に立つでしょう:
- パスワードを紛失したユーザーに送信する「アカウントの回復」URLの生成。
- 非表示のフォームフィールドに保存されたデータが改ざんされていないことの確認。
- 保護されたリソースへの一時的なアクセスを許可するためのワンタイムシークレットURLの生成。例えばユーザーが料金を支払ったダウンロード可能なファイルなど。
SECRET_KEY
と SECRET_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 英数字 を使用することはできません。このアルファベットには英数字、ハイフン、アンダースコアが含まれます。algorithm
はhashlib
がサポートするアルゴリズムでなければなりません。デフォルトは'sha256'
です。fallback_keys
は署名されたデータを検証するために使用する追加の値のリストです。デフォルトはSECRET_KEY_FALLBACKS
です。バージョン 4.2 で非推奨: 位置引数のサポートは非推奨になりました。
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 引数は秘密にする必要はありません。
タイムスタンプ値の検証¶
TimestampSigner
は Signer
のサブクラスで、値に署名されたタイムスタンプを追加します。これにより、署名された値が指定された期間内に作成されたことを確認できます:
>>> 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
オブジェクトを指定します。
バージョン 4.2 で非推奨: 位置引数のサポートは非推奨になりました。
-
複雑なデータ構造の保護¶
リスト、タプル、辞書を保護したい場合は、 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
(秒単位) をチェックします。