【Ed25519】Go言語でJWTに対する署名アルゴリズムの速度を測ってみた

世の中にあるJWTの署名アルゴリズムの速度を計測してみる試みです。
現在、ジョインしているプロジェクトで認証サーバを開発することになったので、JWT署名アルゴリズムはどれを使えばよいかの材料の一つとしてパフォーマンスを調べた次第です。

今回計測したのは「署名の作成速度」と「署名の検証速度」です。

調べたアルゴリズムは以下の5つです。
HS256は共通鍵を使ったもので、他は秘密鍵と公開鍵を使っています。

  • HS256
  • RS256
  • ES256
  • PS256
  • Ed25519

本記事ではそれぞれの署名アルゴリズムの簡易的な説明や、理論上はどうなるべきか?あるいはセキュリティはどうなのか、などについて説明しながらパフォーマンスについて考察します。

コードも公開しますので、各署名アルゴリズムの作成や検証のコードが参考になるかもしれません。

想定している読者は、「JWTの知識があるが、署名アルゴリズムの選び方で迷っている」という読者です。この記事はパフォーマンスに焦点を当てて説明します。

JWTの署名とは

そもそもタイトルに「JWTの署名」とあるのですが、これは何かというと、例えばリクエストに含まれるアクセストークンが偽造/改竄されていないか検証するための仕組みのことです。

JWTの標準仕様はRFC7519で定義されていますが、「署名されたJWT」は呼び方が「JWS(RFC7515)」に変化するので注意が必要です。

また類似するものとして、「暗号化されたJWT」であるJWE(RFC7516)がありますが、本記事では扱いません。

JWSで標準とされている署名について

JWTで利用可能な署名アルゴリズムはRFC 7518の『3.1. “alg” (Algorithm) Header Parameter Values for JWS』で定義されています。

ただし、「none(署名なし)」は通常ありえないので、選択肢から外れます。

本記事で測定するのは、上記のうち暗号のビット長が「256」とされているものです。
また、上記にはないのですが、「Ed25519」が新しいデファクトスタンダードだという情報を目にしたので、それもあわせて測定することにしました。

セキュリティについて:対称鍵署名アルゴリズム(HS256)

これは、共通鍵を使って署名を作成し、共通鍵を使って署名を検証する方式です。

共通鍵が盗まれれば、署名が偽造されることになります。
ですので、後述する非対称系署名アルゴリズムよりセキュリティ的なリスクが高いです。

セキュリティについて:非対称鍵署名アルゴリズム(それ以外)

これは、秘密鍵を使って署名を作成し、公開鍵を使って署名を検証する方法です。

トークンを作成するサーバでだけ秘密鍵を保持すればよいので、影響範囲が狭くなります。
(共通鍵の場合は、トークンを検証するサーバにも共通鍵を安全に配布する必要があります。)

楕円関数を使ったアルゴリズムであるES256は署名の作成でセキュリティホールが見つかったことがあります。(Hackers Describe PS3 Security As Epic Fail, Gain Unrestricted Access

RSA方式を使ったアルゴリズムであるRS256は署名の検証でセキュリティホールが見つかったことがあります。(Wikipedia 『Daniel Bleichenbacher』

Ed25519はそれらのセキュリティホールに対して根本的な解決をしており、セキュリティは強力です。

結論

先に結論を書くと、最も高速なのは当然ですが共通鍵を使ったHS256でした。

秘密鍵と公開鍵を使った非対称鍵系署名アルゴリズムの中ではEd25519が最もパフォーマンスが良かったです。

期待値

署名アルゴリズムは「署名の作成」と「署名の検証」の2つの操作があります。
重要なのは「署名の検証」です。
理由は、「署名の作成」はトークン発行時に1度だけ実行されますが、「署名の検証」はその後何度も実行されるからです。

署名の作成の期待値

最も高速なのは、対称鍵系署名アルゴリズムであるHS256です。
これは共通鍵を利用したアルゴリズムです。

非対称鍵系署名アルゴリズムのうち最も高速なのはEd25519とES256です。
これらは、楕円関数を使ったアルゴリズムを利用しているため、ビット長が短いです。
その分高速な署名の作成が可能です。

Ed25519はES256よりわずかに高速だという情報もあるようですが、ほとんど同じになると思います。
ですので、以下の様になるはずです

HS256 > Ed25519 ≧ ES256 > RS256 = PS256

署名の検証について

署名の検証についても同様にHS256が最速の想定です。
計算がシンプルなアルゴリズムほど速いです。

非対称鍵暗号化方式の中で最も高速なのがEd25519とのことです。

次に高速なのは、RSA暗号を使ったRS256とPS256になるはずです。
楕円関数を使ったES256はアルゴリズムの複雑さから、最も遅くなると聞きました。

HS256 > Ed25519 > RS256 = PS256 > ES256

検証に利用したコード

Go言語を使って測定しました。
これは、私が現在ジョインしているプロジェクトがGoだからです。

署名を作成する処理

以下の関数(sign_benchmark/signature_algorithm.go)を利用します。

func (s signReciever) SignatureAlgorithm(iterations int) (time.Duration, error) {
	startTime := time.Now()
	for i := 0; i < iterations; i++ {
		token := jwt.NewWithClaims(s.method, jwt.MapClaims{
			"name": "John Doe",
			"exp":  time.Now().Add(time.Hour * 72).Unix(),
		})

		if _, err := token.SignedString(s.secretKey); err != nil {
			return 0, errors.Wrap(err, "jwt.Token.SignedString")
		}
	}
	endTime := time.Now()

	duration := endTime.Sub(startTime)
	return duration, nil
}

適当なペイロード(名前と有効期限)だけを持ったJWTにレシーバから受け取った暗号鍵で署名を行います。

引数で試行回数を受け取ります。私は10,000回を指定しました。

署名を検証する処理

以下の関数(sign_benchmark/signature_verification.go)を使います。

func (s signReciever) SignatureVerification(iterations int) (time.Duration, error) {
	// まず署名されたトークンを生成
	token := jwt.NewWithClaims(s.method, jwt.MapClaims{
		"name": "John Doe",
		"exp":  time.Now().Add(time.Hour * 72).Unix(),
	})

	signedToken, err := token.SignedString(s.secretKey)
	if err != nil {
		return 0, errors.Wrap(err, "jwt.Token.SignedString")
	}

	// 生成したトークンを指定された回数だけ検証
	startTime := time.Now()
	for i := 0; i < iterations; i++ {
		_, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) {
			if token.Method != s.method {
				return nil, errors.New("不正な署名方法")
			}
			// RSAまたはRSA-PSSの場合は公開鍵を使用
			switch token.Method.(type) {
			case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
				if publicKey, ok := s.encryptionKey.(*rsa.PublicKey); ok {
					return publicKey, nil
				}
				return nil, errors.New("不適切な公開鍵の型")
			// ECDSAの場合は公開鍵を使用
			case *jwt.SigningMethodECDSA:
				if publicKey, ok := s.encryptionKey.(*ecdsa.PublicKey); ok {
					return publicKey, nil
				}
				return nil, errors.New("不適切な公開鍵の型")
			// Ed25519の場合は公開鍵を使用
			case *jwt.SigningMethodEd25519:
				return s.encryptionKey, nil
			}
			return s.secretKey, nil
		})

		if err != nil {
			return 0, errors.Wrap(err, "jwt.Parse")
		}
	}
	endTime := time.Now()

	duration := endTime.Sub(startTime)
	return duration, nil
}

適当なペイロード(名前と有効期限)だけを持ったJWTにレシーバから受け取った暗号鍵で署名を行い、それを各種署名アルゴリズムで検証します。

引数で試行回数を受け取ります。こちらも10,000回を指定しました。

全体のコード(GitHub)

GitHubで全体のコードを公開します。

GitHub – taako-502/go-sign-benchmarkin…

GitHub – taako-502/go-sign-benchmarkin…

JWTの署名アルゴリズムのベンチマークを測定する. Contribute to taako-502/go-sign-benchmarking development by creating an account on GitHub.

JWTの署名アルゴリズムのベンチマークを測定する. Contribute to taako-502/go-sign-benchmarking development by creating an account on GitHub.

実行結果

署名を作成する処理

署名アルゴリズム速度(ミリ秒)
HS25616.804
Ed25519243.223
RS2569152.258
ES256260.088
PS2569369.411

結論、以下の様になりました。

HS256 > Ed25519 ≧ ES256 > RS256 ≧ PS256

これは予想通りの結果です。

共通鍵を使った方式は、Ed25519の約7.9倍高速でした。
Ed25519とES256はだいたい同じ速度です。

RSA暗号を使ったRS256およびPS256はEd2551に比べて61倍ほど低速でした。
これは、ユーザ数が増えた場合、大きなボトルネックになる可能性があります。

署名を検証する処理

署名アルゴリズム速度(ミリ秒)
HS25622.403
Ed25519553.173
RS256310.573
ES256602.008
PS256348.269

HS256 > Ed25519 > ES256 > RS256 ≧ PS256

予想と違ったのはES256の結果でした。
私は、ES256は署名の検証速度が遅いと聞いていたのですが、実際にはRSA方式であるRS256とPS256より高速でした。

それ以外は予想通りで、共通鍵を使ったHS256が最も高速で、Ed25519が次に高速でした。

まとめ

パフォーマンスの観点ではHS256が最速です。
しかし、共通鍵は鍵の管理が難しいです。
もし共通鍵が流出すれば、簡単に署名が偽造されます。

セキュリティ強度は今回計測した中でEd25519が最も強いです。
また、RSAに比べて暗号のビット長も短いですし、アルゴリズムはES256よりもシンプルです。

パフォーマンスの観点でも非対称系アルゴリズムの中ではEd25519が最も良い結果になりました。

参考資料

OAuth アクセストークンの実装に関する考察 – Qiita

OAuth アクセストークンの実装に関する考察 – Qiita

はじめに この記事では、OAuth 2.0 のアクセストークンの実装に関する考察を行います。本記事の執筆者本人による動画解説も『OAuth & OIDC 勉強会 【アクセストークン編】』で公開しておりますので、併せてご参照ください。…

はじめに この記事では、OAuth 2.0 のアクセストークンの実装に関する考察を行います。本記事の執筆者本人による動画解説も『OAuth & OIDC 勉強会 【アクセストークン編】』で公開しておりますので、併せてご参照ください。…