HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

PHPでJWTを扱うためのライブラリのサンプルと処理時間計測

はじめに

以下の記事を見ていて、本当に検証に使用したプログラムが載っているんだろうか?とか疑問が生じたのと、現環境でNamshi/JOSEが動かなかったので、別のライブラリ3つほどを使って実測してみたメモ。

※Namshi/JOSEは、単に楕円曲線をサポートしていないだけで、動きはしたので、追加。

qiita.com

目次

元記事のプログラムの問題点

載っている検証プログラムの何が問題かというと・・・

  • "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を使用した。

qiita.com

firebase/php-jwt

github.com

インストールはcomposerで行う。

$ composer require firebase/php-jwt

バージョンは「6.10」がインストールされた。

lcobucci/jwt

github.com

インストールはcomposerで行う。

$ composer require lcobucci/jwt

バージョンは「5.3」がインストールされた。

adhocore/jwt

github.com

インストールはcomposerで行う。

$ composer require adhocore/jwt

バージョンは「1.1」がインストールされた。

namshi/jose

github.com

インストールは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.