はじめに
以下の記事を見ていて、本当に検証に使用したプログラムが載っているんだろうか?とか疑問が生じたのと、現環境でNamshi/JOSEが動かなかったので、別のライブラリ3つほどを使って実測してみたメモ。
※Namshi/JOSEは、単に楕円曲線をサポートしていないだけで、動きはしたので、追加。
目次
元記事のプログラムの問題点
載っている検証プログラムの何が問題かというと・・・
"ES512"=>"ec521"
となっているので、そんなファイルはないと言われる(正しくは「ec512
」)。- 検証するアルゴリズムのリストで二重ループしているので、「RS256で署名したものをES384で検証する」などの意味不明なことをしている。
検証環境
- ホスト機はAMD Ryzen 5 7530U with Radeon Graphics 2.00 GHzを搭載したHP ProBook
- VirtualBox 7.0.14
- ゲスト機はAlmaLinux release 9.3 (Shamrock Pampas Cat)
- PHP 8.3.6
使用したJWTライブラリ
こちらの記事で紹介されているものプラス1プラスNamshi/JOSEを使用した。
firebase/php-jwt
インストールはcomposerで行う。
$ composer require firebase/php-jwt
バージョンは「6.10」がインストールされた。
lcobucci/jwt
インストールはcomposerで行う。
$ composer require lcobucci/jwt
バージョンは「5.3」がインストールされた。
adhocore/jwt
インストールはcomposerで行う。
$ composer require adhocore/jwt
バージョンは「1.1」がインストールされた。
namshi/jose
インストールはcomposerで行う。
$ composer require namshi/jose
バージョンは「7.2」がインストールされた。
秘密鍵/公開鍵の作成
元記事のものに加えて、RSA(4096bit)のキーも追加。
#! /bin/bash #楕円曲線 256bit openssl ecparam -genkey -name prime256v1 -noout -out ec256-key-pair.pem openssl ec -in ec256-key-pair.pem -outform PEM -pubout -out ec256-key-pub.pem openssl ec -in ec256-key-pair.pem -outform PEM -out ec256-key-pri.pem #楕円曲線 384bit openssl ecparam -genkey -name secp384r1 -noout -out ec384-key-pair.pem openssl ec -in ec384-key-pair.pem -outform PEM -pubout -out ec384-key-pub.pem openssl ec -in ec384-key-pair.pem -outform PEM -out ec384-key-pri.pem #楕円曲線 512bit openssl ecparam -genkey -name secp521r1 -noout -out ec512-key-pair.pem openssl ec -in ec512-key-pair.pem -outform PEM -pubout -out ec512-key-pub.pem openssl ec -in ec512-key-pair.pem -outform PEM -out ec512-key-pri.pem #RSA 2048bit openssl genpkey -algorithm RSA -out rsa2048-key-pri.pem -pkeyopt rsa_keygen_bits:2048 openssl rsa -pubout -in rsa2048-key-pri.pem -out rsa2048-key-pub.pem #RSA 4096bit openssl genpkey -algorithm RSA -out rsa4096-key-pri.pem -pkeyopt rsa_keygen_bits:4096 openssl rsa -pubout -in rsa4096-key-pri.pem -out rsa4096-key-pub.pem
検証プログラム
元記事のプログラムの基本構造は保ちつつ、各ライブラリでの処理を比較出力できるように改修。
<?php //jwt-bench.php require_once './vendor/autoload.php'; use Firebase\JWT\Key; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\IssuedBy; use Lcobucci\JWT\Validation\Constraint\RelatedTo; use Namshi\JOSE\SimpleJWS; class FirebaseJWT { private string $signer; private string $private_key; private string $public_key; private $validatedToken; public function __construct($name, $key) { $this->signer = $name; $this->private_key = file_get_contents("${key}-key-pri.pem"); $this->public_key = file_get_contents("${key}-key-pub.pem"); } // tokenの生成 public function sign($data): string { $payload = [ 'iss' => 'hogehoge', 'sub' => 'Hello!', 'name' => 'fugafuga', 'admin' => true, 'iat' => time(), 'data' => $data, ]; return Firebase\JWT\JWT::encode($payload, $this->private_key, $this->signer); } // tokenの検証 public function validate(string $jwt): bool { try { // 署名検証 $decoded = Firebase\JWT\JWT::decode($jwt, new Key($this->public_key, $this->signer)); // iss検証 if ($decoded->iss !== 'hogehoge') { throw new Exception('iss error'); } // sub検証 if ($decoded->sub !== 'Hello!') { throw new Exception('sub error'); } $this->validatedToken = $decoded; } catch (Exception $e) { return false; } return true; } // tokenからデータを取得 public function getDataArray(string $jwt): array { return (array)$this->validatedToken; } public function getData(string $jwt, string $key) { $ary = $this->getDataArray($jwt); return $ary[$key]; } } class LcobucciJWT { private Configuration $config; private $validatedToken; public function __construct($name, $key) { $signer; if ($name === 'ES256') { $signer = new Lcobucci\JWT\Signer\Ecdsa\Sha256(); } else if ($name === 'ES384') { $signer = new Lcobucci\JWT\Signer\Ecdsa\Sha384(); } else if ($name === 'ES512') { $signer = new Lcobucci\JWT\Signer\Ecdsa\Sha512(); } else if ($name === 'RS256') { $signer = new Lcobucci\JWT\Signer\Rsa\Sha256(); } else if ($name === 'RS512') { $signer = new Lcobucci\JWT\Signer\Rsa\Sha512(); } else { throw new Exception($name.': Invalid algorithm.'); } $this->config = Configuration::forAsymmetricSigner( $signer, InMemory::file("${key}-key-pri.pem"), InMemory::file("${key}-key-pub.pem") ); } // tokenの生成 public function sign($data): string { $token = $this->config->builder() ->issuedBy('hogehoge') // iss ->relatedTo('Hello!') // sub ->withClaim('name', 'fugafuga') // name(パブリッククレーム) ->withClaim('admin', true) // admin(プライベートクレーム) ->issuedAt(new DateTimeImmutable()) // iat ->withClaim('data', $data) // data(プライベートクレーム) ->getToken($this->config->signer(), $this->config->signingKey()); return $token->toString(); } // tokenの検証 public function validate(string $jwt): bool { $token = $this->config->parser()->parse($jwt); $this->config->setValidationConstraints(...[ // 署名検証 new SignedWith($this->config->signer(), $this->config->verificationKey()), // iss検証 new IssuedBy('hogehoge'), // sub検証 new RelatedTo('Hello!') ]); // バリデーションエラーを例外で返したい場合はassert()を使用する $res = $this->config->validator()->validate($token, ...$this->config->validationConstraints()); if ($res === true) { $this->validatedToken = $token; } return $res; } // tokenからデータを取得 public function getDataArray(string $jwt): array { $token = $this->validatedToken; return $token->claims()->all(); } public function getData(string $jwt, string $key) { $ary = $this->getDataArray($jwt); return $ary[$key]; } } class AdhocoreJWT { private $jwt; private array $decodedPayload; public function __construct($name, $key) { $this->jwt = new Ahc\Jwt\JWT("${key}-key-pri.pem", $name); $this->jwt->registerKeys([$name => "${key}-key-pub.pem"]); } // tokenの生成 public function sign($data): string { $payload = [ 'iss' => 'hogehoge', 'sub' => 'Hello!', 'name' => 'fugafuga', 'admin' => true, 'iat' => time(), 'data' => $data, ]; return $this->jwt->encode($payload); } // tokenの検証 public function validate(string $jwt): bool { try { // 署名検証 $payload = $this->jwt->decode($jwt, true); // iss検証 if (!isset($payload['iss']) || $payload['iss'] !== 'hogehoge') { throw new Exception('iss error'); } // sub検証 if (!isset($payload['sub']) || $payload['sub'] !== 'Hello!') { throw new Exception('sub error'); } $this->decodedPayload = $payload; } catch (Exception $e) { return false; } return true; } // tokenからデータを取得 public function getDataArray(string $jwt): array { return $this->decodedPayload; } public function getData(string $jwt, string $key) { $ary = $this->getDataArray($jwt); return $ary[$key]; } } class NamshiJOSE { private string $type; private string $public_key; private string $private_key; private array $validatedPayload; public function __construct($name, $key) { $this->type = $name; $this->private_key = file_get_contents("${key}-key-pri.pem"); $this->public_key = file_get_contents("${key}-key-pub.pem"); } // tokenの生成 public function sign($data): string { $date = new DateTime('+7 days'); $jws = new SimpleJWS(array( 'alg' => $this->type, 'exp' => $date->format('U') )); $jws->setPayload( ["data" => $data] ); $privateKey = openssl_pkey_get_private($this->private_key, ""); /* * Namshi/JOSEの実装: * $resource = openssl_pkey_get_public($key) ?: openssl_pkey_get_private($key, $password); * の問題で、 * PHP Warning: openssl_pkey_get_public(): Don't know how to get public key from this private key * in vendor/namshi/jose/src/Namshi/JOSE/Signer/OpenSSL/PublicKey.php on line 63 * という警告を吐くので、警告を抑制。 */ @$jws->sign($privateKey); return $jws->getTokenString(); } // tokenの検証 public function validate(string $jwt): bool { try { $jws = SimpleJWS::load($jwt); $public_key = openssl_pkey_get_public($this->public_key); if ($jws->isValid($public_key, $this->type)) { $payload = $jws->getPayload(); $this->validatedPayload = $payload; return true; } else { return false; } } catch (Exception $e) { return false; } } // tokenからデータを取得 public function getDataArray(string $jwt): array { return $this->validatedPayload; } public function getData(string $jwt, string $key) { return $this->validatedPayload[$key]; } } function repeat($str, $n) { $res = ""; for ($i = 0; $i < $n; ++$i) { $res .= $str; } return $res; } date_default_timezone_set('Asia/Tokyo'); const LOOP_COUNT = 1000; $keys = [ "ES256" => "ec256", "ES384" => "ec384", "ES512" => "ec512", "RS256" => "rsa2048", "RS512" => "rsa4096", ]; $base = "1234567890"; $testobjs = [ repeat($base, 1), repeat($base, 10), repeat($base, 50), ]; $clses = [ "FirebaseJWT", "LcobucciJWT", "AdhocoreJWT", "NamshiJOSE", ]; foreach ($testobjs as $obj) { print "\n"; print "#### object length:" . strlen($obj) ."\n"; print "\n"; print "|ライブラリ名|アルゴリズム名|トークン長|署名時間(秒)|検証時間(秒)|エラー|\n"; print "|:---|:---|---:|---:|---:|:---|\n"; foreach ($clses as $cls) { foreach ($keys as $name => $key) { print "|" . $cls . "|" . $name . "|"; try { $jwtGenerator = new $cls($name, $key); $token = $jwtGenerator->sign($obj); print strlen($token) . "|"; $start = microtime(true); for ($i = 0; $i < LOOP_COUNT; ++$i) { $token = $jwtGenerator->sign($obj); } $end = microtime(true) - $start; print number_format($end, 2) . "|"; } catch (Exception $e) { print "|||" . $e->getMessage() . "|" . "\n"; continue; } try { $jwtGenerator = new $cls($name, $key); $v = $jwtGenerator->validate($token); if ($v === true) { $d = $jwtGenerator->getData($token, 'data'); if ($d === $obj) { $start = microtime(true); for ($i = 0; $i < LOOP_COUNT; ++$i) { $v = $jwtGenerator->validate($token); $dmy = $jwtGenerator->getData($token, 'data'); } $end = microtime(true) - $start; print number_format($end, 2) . "||\n"; } else { print "||\n"; } } else { print "||\n"; } } catch (Exception $e) { print "|" . $e->getMessage() . "|" . "\n"; } } } }
計測結果
計測は、ループを1000回回した時のトータル時間で表示。
object length:10
ライブラリ名 | アルゴリズム名 | トークン長 | 署名時間(秒) | 検証時間(秒) | エラー |
---|---|---|---|---|---|
FirebaseJWT | ES256 | 259 | 0.56 | 0.32 | |
FirebaseJWT | ES384 | 301 | 1.20 | 0.81 | |
FirebaseJWT | ES512 | Algorithm not supported | |||
FirebaseJWT | RS256 | 515 | 1.52 | 0.27 | |
FirebaseJWT | RS512 | 856 | 6.67 | 0.34 | |
LcobucciJWT | ES256 | 267 | 0.41 | 0.42 | |
LcobucciJWT | ES384 | 310 | 1.02 | 0.90 | |
LcobucciJWT | ES512 | 357 | 0.65 | 0.83 | |
LcobucciJWT | RS256 | 524 | 1.38 | 0.38 | |
LcobucciJWT | RS512 | 865 | 6.27 | 0.43 | |
AdhocoreJWT | ES256 | Unsupported algo ES256 | |||
AdhocoreJWT | ES384 | Unsupported algo ES384 | |||
AdhocoreJWT | ES512 | Unsupported algo ES512 | |||
AdhocoreJWT | RS256 | 515 | 0.76 | 0.37 | |
AdhocoreJWT | RS512 | 856 | 4.93 | 0.44 | |
NamshiJOSE | ES256 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | ES384 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | ES512 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | RS256 | 457 | 1.38 | 0.35 | |
NamshiJOSE | RS512 | 798 | 6.25 | 0.44 |
object length:100
ライブラリ名 | アルゴリズム名 | トークン長 | 署名時間(秒) | 検証時間(秒) | エラー |
---|---|---|---|---|---|
FirebaseJWT | ES256 | 379 | 0.56 | 0.33 | |
FirebaseJWT | ES384 | 421 | 1.22 | 0.86 | |
FirebaseJWT | ES512 | Algorithm not supported | |||
FirebaseJWT | RS256 | 635 | 1.53 | 0.28 | |
FirebaseJWT | RS512 | 976 | 6.56 | 0.35 | |
LcobucciJWT | ES256 | 388 | 0.39 | 0.41 | |
LcobucciJWT | ES384 | 430 | 1.04 | 0.91 | |
LcobucciJWT | ES512 | 478 | 0.64 | 0.86 | |
LcobucciJWT | RS256 | 644 | 1.38 | 0.38 | |
LcobucciJWT | RS512 | 985 | 6.28 | 0.44 | |
AdhocoreJWT | ES256 | Unsupported algo ES256 | |||
AdhocoreJWT | ES384 | Unsupported algo ES384 | |||
AdhocoreJWT | ES512 | Unsupported algo ES512 | |||
AdhocoreJWT | RS256 | 635 | 0.71 | 0.37 | |
AdhocoreJWT | RS512 | 976 | 5.05 | 0.44 | |
NamshiJOSE | ES256 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | ES384 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | ES512 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | RS256 | 577 | 1.39 | 0.35 | |
NamshiJOSE | RS512 | 918 | 6.29 | 0.42 |
object length:500
ライブラリ名 | アルゴリズム名 | トークン長 | 署名時間(秒) | 検証時間(秒) | エラー |
---|---|---|---|---|---|
FirebaseJWT | ES256 | 912 | 0.57 | 0.33 | |
FirebaseJWT | ES384 | 954 | 1.35 | 0.83 | |
FirebaseJWT | ES512 | Algorithm not supported | |||
FirebaseJWT | RS256 | 1168 | 1.54 | 0.28 | |
FirebaseJWT | RS512 | 1509 | 6.46 | 0.34 | |
LcobucciJWT | ES256 | 922 | 0.39 | 0.43 | |
LcobucciJWT | ES384 | 964 | 1.04 | 0.92 | |
LcobucciJWT | ES512 | 1012 | 0.67 | 0.95 | |
LcobucciJWT | RS256 | 1176 | 1.40 | 0.37 | |
LcobucciJWT | RS512 | 1519 | 6.28 | 0.44 | |
AdhocoreJWT | ES256 | Unsupported algo ES256 | |||
AdhocoreJWT | ES384 | Unsupported algo ES384 | |||
AdhocoreJWT | ES512 | Unsupported algo ES512 | |||
AdhocoreJWT | RS256 | 1168 | 0.71 | 0.38 | |
AdhocoreJWT | RS512 | 1509 | 4.98 | 0.45 | |
NamshiJOSE | ES256 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | ES384 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | ES512 | phpseclib 1.0.0(LTS), even the latest 2.0.0, doesn't support PHP7 yet | |||
NamshiJOSE | RS256 | 1110 | 1.42 | 0.36 | |
NamshiJOSE | RS512 | 1451 | 6.44 | 0.45 |
参考
JWT関連のライブラリと思われるものを探した結果。
$ composer search jwt adhocore/jwt Ultra lightweight JSON web token (JWT) library for PHP5.5+. firebase/php-jwt A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec. lcobucci/jwt A simple library to work with JSON Web Token and JSON Web Signature namshi/jose JSON Object Signing and Encryption library for PHP. lcobucci/jwt A simple library to work with JSON Web Token and JSON Web Signature firebase/php-jwt A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec. tymon/jwt-auth JSON Web Token Authentication for Laravel and Lumen lexik/jwt-authentication-bundle This bundle provides JWT authentication for your Symfony REST API namshi/jose JSON Object Signing and Encryption library for PHP. web-token/jwt-framework JSON Object Signing and Encryption library for PHP and Symfony Bundle. php-open-source-saver/jwt-auth JSON Web Token Authentication for Laravel and Lumen gesdinet/jwt-refresh-token-bundle Implements a refresh token system over Json Web Tokens in Symfony auth0/auth0-php PHP SDK for Auth0 Authentication and Management APIs. adhocore/jwt Ultra lightweight JSON web token (JWT) library for PHP5.5+. web-token/jwt-util-ecc ! Abandoned ! ECC Tools for the JWT Framework. web-token/jwt-signature-algorithm-ecdsa ! Abandoned ! ECDSA Based Signature Algorithms the JWT Framework. web-token/jwt-signature ! Abandoned ! Signature component of the JWT Framework. web-token/jwt-key-mgmt ! Abandoned ! Key Management component of the JWT Framework. web-token/jwt-core ! Abandoned ! Core component of the JWT Framework.