web アプリケーションのセキュリティを確保する際、認証・認可にはさまざまな選択肢があります。特に人気があるのは PASETO と JWT です。
JWT は何年も前から広く利用されてきましたが、PASETO は比較的新しい技術であり、その高度なセキュリティ機能から注目を集めています。
PASETO(Platform-Agnostic Security Tokens)は、セキュアなステートレス・トークンの仕様およびリファレンス実装であり、JWT に代わる安全性の高い技術です。
トークン・ベース認証の理解
セキュアな API でユーザーを認証するための典型的なフローは以下です。
- ユーザーは認証のためにユーザー名とパスワードを提供する。
- 認証に成功すると、API はアクセストークン(JWT または PASETO)を返す。
- アクセストークンは、保護されたエンドポイントへのリクエスト時に Authorization ヘッダーに含める。
- API サーバーはトークンを検証し、アクセスが有効であれば適切に保護されたデータで応答する。
JWT と制限の理解
JWT は 3 つのコンポーネントで構成されます。
- ヘッダー:トークンの署名アルゴリズムを含む。
- ペイロード:認証されたユーザーに関する情報と追加データを保持する。サーバーはペイロードのこの部分をカスタマイズできる。
- 署名:秘密鍵を使用してサーバーが生成する。この署名によって、サーバーは検証プロセス中に JWT の真正性を検証できる。
Photo by Wallarm
JWT はデジタル署名アルゴリズムと検証の実装の選択に柔軟性を提供しますが、この柔軟性は脆弱性ももたらします。
ECDSA(Invalid-Curve Attack に弱い)や RSA PKCSv1.5(Padding Oracle Attack に弱い)のように、弱く攻撃されやすいアルゴリズムが複数あります。
JWT の実装はエラーも起こしやすく、JWT 検証の破綻のようなセキュリティの脆弱性をもたらす可能性があります。
JWT を正しく使用すれば、信頼性が高く柔軟な認証システムになります。しかし、サーバーが潜在的な攻撃にさらされないよう、注意を払う必要があります。
これに対して PASETO は、実装プロセスを単純化することで、これらの問題に対処しています。
PASETO Solution
JWT が柔軟な実装を提供するのに対し、PASETO はより厳格なアプローチをとります。しかし、この厳格さが実装エラーや誤用を防ぐのに役立っています。
PASETO はユーザーフレンドリーに設計されており、JWT と比較して高い cryptographic resilience を提供します。
PASETO を使用する場合、ユーザーは 2 つの設定
- PASETO のバージョン(
v1
,v2
,v3
, orv4
)を トークンのversion
フィールドで指定する。 - 暗号化と復号化を対称にするか非対称にするかを、トークンの
purpose
フィールドで指定する。
を行うだけです。
PASETO Token の構造
JSON Web Token(JWT)と同様に、PASETO Token はドット区切りの base64url エンコードされたデータで構成され、以下の形式で編成されます:
version.purpose.payload.footer
version
: Token 形式のインクリメンタルな改良を許可する。2024 年 7 月現在のバージョンは "v1"、"v2"、"v3"、"v4 "です。v1
: 現在広く利用可能な強力な暗号理論を使用。v2
: より新しく強力な暗号理論を利用するが、サポートする暗号ライブラリの数は少ない。
purpose
: Token の形式を "local "または "public "のどちらかを表す簡潔な文字列。local
: Token のペイロードは暗号化され、共有鍵を持つパーティのみがアクセスできる。public
: ペイロードは暗号化されず、公開鍵を使って署名・検証される。
payload
: Token のバージョンと目的に応じてエンコードされたデータ。footer
(optional): 暗号化されていない JSON。通常、トークン検証のための公開鍵の ID を格納するために使用される。
すべての PASETO Token 形式は改ざん防止に対応しており、トークンに変更が加えられた場合、検証は失敗します。
Local Token (対称暗号化)
Local Token は常に共有秘密鍵を用いて対称暗号化される。つまり、Local PASETO Token の内容は、正しい秘密鍵なしには見ることができません。
これはデコードされたペイロード、オプションのフッター、情報に署名するために使用される署名鍵を含む、Local PASETO Token の例です。
Public Token (非対称暗号化)
Public PASETO Token は、関係者全員と秘密鍵を共有するのが安全でないシナリオに適しています。
Public Token は暗号化されませんが、デジタル署名されます。つまり、攻撃者が Public PASETO Token を入手した場合、PASETO Token に使用されているデジタル署名により、その内容を閲覧することはできますが、検出されずに変更することはできません。
悪意を持って変更された Public PASETO Token を検証しようとすると、エラーが発生します。
以下は、デコードされたペイロード、オプションのフッター、情報に署名するために使用される公開鍵と秘密鍵を含む、Public PASETO Token の例です。
Versions
PASETO の各バージョンは、前のバージョンよりも改良されています。PASETO を仕様通りに正しく実装するには、バージョンごとに提供されているドキュメントを参照してください。
https://github.com/paseto-standard/paseto-spec/tree/master/docs/01-Protocol-Versions
Libraries
以下のリンクには、一般的なすべての言語とそのサポートバージョンの PASETO を実装したライブラリがあります。
Ruby ライブラリを使った実装
PASETO を実装した Ruby ライブラリの使用例を以下に示します。
https://github.com/bannable/paseto
事前に gem のインストールが必要です。
gem 'ruby-paseto'
gem 'rbnacl', '~> 7.1.1' # optional - PASETO version 4 を使う場合のみ使用
対称暗号化 (local)
require 'paseto'
####################
####### 暗号化 ######
####################
# 通常、この32バイトの共有鍵は、認証サーバーとクライアントサーバーの両方に格納される。
shared_secret_key = SecureRandom.bytes(32)
# PASETO 暗号化/復号化の初期化をする。
crypt = Paseto::V4::Local.new(ikm: shared_secret_key) # version: v4 / 用途: local
# 平文のペイロード
claims = { "company" => "monstarlab" }
footer = { "viewable" => "yes" }
# ペイロードをエンコードし、PASETOを取得する。
encrypted_token = crypt.encode(claims, footer: JSON.dump(footer))
# => "v4.local.E1Y_KQ6Ek8lSOKrJ6kI1YjWXfAKJ0OEcdhUPywznjBjK5SGDUr4-6rbaZk-CIM_mdQgQHGPj8yAWQswktkCe_Sm_Nj9eEfDxNBFeAC2KsgqFCjF07VJo5ail0jnSTNM0-PekMytJleea8OvNkKdoLs4GKAsZTTJ_-DEMmOMyVlddlmaWoVwnF2JkpjBzFRO7d6PlIIY29rQWOXSvZxoLEqkE5XJvHpFs4NTuCHnF4Pko10X_sgHCPkTGXkWNDg.eyJ2aWV3YWJsZSI6InllcyJ9"
####################
####### 復号化 ######
####################
# 通常、この32バイトの共有鍵は、認証サーバーとクライアントサーバーの 両方に保存される。
shared_secret_key = SecureRandom.bytes(32)
# PASETO 暗号化/復号化の初期化をする。
crypt = Paseto::V4::Local.new(ikm: shared_secret_key) # version: v4 / 用途: local
encrypted_token = "v4.local.E1Y_KQ6Ek8lSOKrJ6kI1YjWXfAKJ0OEcdhUPywznjBjK5SGDUr4-6rbaZk-CIM_mdQgQHGPj8yAWQswktkCe_Sm_Nj9eEfDxNBFeAC2KsgqFCjF07VJo5ail0jnSTNM0-PekMytJleea8OvNkKdoLs4GKAsZTTJ_-DEMmOMyVlddlmaWoVwnF2JkpjBzFRO7d6PlIIY29rQWOXSvZxoLEqkE5XJvHpFs4NTuCHnF4Pko10X_sgHCPkTGXkWNDg.eyJ2aWV3YWJsZSI6InllcyJ9"
# トークン `eyJ2aWV3YWJsZSI6InllcyJ9` の最後の部分は、base64エンコードされた文字列であることに注意。
# つまり、誰でもその内容を見ることができる。
Base64.decode64("eyJ2aWV3YWJsZSI6InllcyJ9")
# => "{\"viewable\":\"yes\"}"
# トークンをデコードし、ペイロードを取得する。
crypt.decode(encrypted_token)
# => <Paseto::Result
# claims={
# "exp"=>"2023-05-10T11:18:41+09:00",
# "iat"=>"2023-05-10T10:18:41+09:00",
# "nbf"=>"2023-05-10T10:18:41+09:00",
# "company"=>"monstarlab"},
# footer={"viewable"=>"yes"}
# >
# トークンが悪意を持って変更された場合、エラーが発生する。
encrypted_token[-1] = "M"
crypt.decode(encrypted_token)
# Paseto::InvalidAuthenticator: Paseto::InvalidAuthenticator
# from /Users/tony_duong/.rvm/gems/ruby-3.1.3/gems/ruby-paseto-0.1.2/lib/paseto/symmetric_key.rb:53:in `decrypt'
非対称暗号化(公開)の使い方
公開鍵と秘密鍵のペアの生成が必要です。
ssh-keygen
# Output: 公開鍵と秘密鍵
require 'paseto'
####################
###### 暗号化 #######
####################
# PASETO 暗号化/復号化の初期化をする。
pem = File.read('private_key')
signer = Paseto::V4::Public.new(pem)
# 平文のペイロード
claims = { "company" => "monstarlab" }
footer = { "viewable" => "yes" }
# ペイロードをエンコードし、PASETOを取得する。
signed_token = signer.encode(claims, footer: footer)
# => "v4.public.eyJleHAiOiIyMDIzLTA1LTEwVDExOjQ2OjA0KzA5OjAwIiwiaWF0IjoiMjAyMy0wNS0xMFQxMDo0NjowNCswOTowMCIsIm5iZiI6IjIwMjMtMDUtMTBUMTA6NDY6MDQrMDk6MDAiLCJjb21wYW55IjoibW9uc3RhcmxhYiJ9taKQPCARAZHv85xk7yaWPDeWHaHt981eHmoiYIrIcA-monnIbMax2EDxIjObgr6qLLuYzAH4BK5N6q0TJANeBg.eyJ2aWV3YWJsZSI6InllcyJ9"
####################
###### 復号化 #######
####################
verifier = Paseto::V4::Public.new('public_key')
# 公開鍵で初期化された場合、検証/復号のみ実行可能。
# エンコードが呼び出されるとエラーが発生する。
verifier.encode({'foo' => 'bar'})
# => ArgumentError
signed_token = "v4.public.eyJleHAiOiIyMDIzLTA1LTEwVDExOjQ2OjA0KzA5OjAwIiwiaWF0IjoiMjAyMy0wNS0xMFQxMDo0NjowNCswOTowMCIsIm5iZiI6IjIwMjMtMDUtMTBUMTA6NDY6MDQrMDk6MDAiLCJjb21wYW55IjoibW9uc3RhcmxhYiJ9taKQPCARAZHv85xk7yaWPDeWHaHt981eHmoiYIrIcA-monnIbMax2EDxIjObgr6qLLuYzAH4BK5N6q0TJANeBg.eyJ2aWV3YWJsZSI6InllcyJ9"
verifier.decode(signed_token)
# => <Paseto::Result
# claims={
# "exp"=>"2023-05-10T11:18:41+09:00",
# "iat"=>"2023-05-10T10:18:41+09:00",
# "nbf"=>"2023-05-10T10:18:41+09:00",
# "company"=>"monstarlab"},
# footer={"viewable"=>"yes"}
# >
結論
JSON Web Token(JWT)の不注意な使用から発生する可能性のある脆弱性について説明しました。JWT は、正しく使用されれば、認証をシステムに組み込む効果的な手段として機能しますが、その柔軟な仕様は、潜在的に実装エラーやその後のセキュリティ問題を引き起こす可能性があります。
このような懸念に対処するため、PASETO は、特定の設計目標を念頭に置いた代替ソリューションとして導入されました。
- 使用方法の簡素化: PASETO は、
purpose
とversion
のパラメータを指定するだけで、トークン作成プロセスを簡素化します。 - 実装エラーへの耐性: JWT とは異なり、PASETO では安全でない可能性のある暗号アルゴリズムを広範囲から選択する必要がないため、実装ミスのリスクを軽減することができます。
PASETO は、開発者の意思決定プロセスを合理化することで、セキュリティ・トークンに対する開発者ファーストのアプローチを取っています。対称または非対称のセキュリティモデルという 2 つの異なる目的を提供することで、PASETO は認証された暗号化とデジタル署名に最適なオプションを自動的に選択します。これにより、トークンの安全性が確保され、暗号の脆弱性を回避することができます。
全体として、PASETO はセキュリティー・トークン管理により強固でわかりやすいアプローチを提供し、JWT に関連するリスクを軽減すると同時に、システムのセキュリティーを高いレベルで維持します。
References
- PASETO: The JWT Killer? - article by Sandesh Dahake
- PASETO: Platform-Agnostic Security Tokens - Github repository
- Why PASETO is better than JWT for token-based authentication? - Youtube video by Tech School
- Introducing JPaseto: Security Tokens For Java - article by Brian Demers
- Encode or Decode PASETO
- Paseto - Github repository
Article Photo by Erik Mclean