Initial conversion from Misuzu.

This commit is contained in:
flash 2025-05-08 22:05:29 +02:00
commit d811e4d6a1
Signed by: flash
GPG key ID: 6D833BE0D210AC02
51 changed files with 20301 additions and 0 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab

5
.gitattributes vendored Normal file
View file

@ -0,0 +1,5 @@
* text=auto
*.sh text eol=lf
*.php text eol=lf
*.bat text eol=crlf
VERSION text eol=lf

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
[Tt]humbs.db
[Dd]esktop.ini
.DS_Store
.vscode/
.vs/
.idea/
docs/html/
.phpdoc*
.phpunit*
vendor/

30
LICENCE Normal file
View file

@ -0,0 +1,30 @@
Copyright (c) 2025, flashwave <me@flash.moe>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted (subject to the limitations in the disclaimer
below) provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

42
Makefile Normal file
View file

@ -0,0 +1,42 @@
ROOT := $(realpath .)
export ROOT
PHPSTAN_CONFIG := $(realpath phpstan.neon)
export PHPSTAN_CONFIG
PHPSTAN_LEGACY_CONFIG := $(realpath phpstan-legacy.neon)
export PHPSTAN_LEGACY_CONFIG
SRC_DIR := $(realpath src)
export SRC_DIR
DEV_TARGETS := install update analyse analyze
DEV_DIR := $(realpath dev)
DEV_DIRS := $(wildcard ${DEV_DIR}/*)
# We'll use the lowest common denominator to maintain the root packages
PHP_506 := $(shell which php5.6)
export PHP_506
COMPOSER_LTS := $(shell which composer-lts)
export COMPOSER_LTS
SUDO = $(shell which sudo)
USER_NAME = $(shell id -un)
GROUP_NAME = $(shell id -gn)
${DEV_TARGETS}: ${DEV_DIRS}
install:
${PHP_506} ${COMPOSER_LTS} install
update:
${PHP_506} ${COMPOSER_LTS} update
fix-perms: # WSL skill issues
${SUDO} chown -R ${USER_NAME}:${GROUP_NAME} .
${DEV_DIRS}:
${MAKE} -C $@ ${MAKECMDGOALS}
.PHONY: ${DEV_TARGETS} ${DEV_DIRS} fix-perms

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# Railgun JWT
This is a JWT library that exists because I ran into issues with the more commonly used Firebase JWT library.
It is very, very loosely based on it in but you'll probably not run into many familiar things.
I'll write a better readme Someday...
## Why does this target PHP 5.6?????
The only core dependency this library has is PHP Sec Lib, which also targets PHP 5.6.
Although I'm also doing it as a personal challenge to see if I can support this insane setup.

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.0.0

27
composer.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "railgun/jwt",
"description": "A modular JWT library.",
"type": "library",
"homepage": "https://railgun.sh/libs/jwt",
"license": "bsd-3-clause-clear",
"require": {
"php": ">=5.6",
"phpseclib/phpseclib": "~3.0"
},
"authors": [
{
"name": "flashwave",
"email": "packagist@flash.moe",
"homepage": "https://flash.moe",
"role": "mom"
}
],
"autoload": {
"psr-4": {
"Railgun\\Jwt\\": "src"
},
"files": [
"src/polyfill.php"
]
}
}

252
composer.lock generated Normal file
View file

@ -0,0 +1,252 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "69fe58fe65d92e48451e103ec91a85a2",
"packages": [
{
"name": "paragonie/constant_time_encoding",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "317718fb438e60151f72b20404f040cb5ae1d494"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/317718fb438e60151f72b20404f040cb5ae1d494",
"reference": "317718fb438e60151f72b20404f040cb5ae1d494",
"shasum": ""
},
"require": {
"php": "^5.3|^7|^8"
},
"require-dev": {
"paragonie/random_compat": "^1.4|^2",
"phpunit/phpunit": ">= 4"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2022-01-17T05:23:46+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v2.0.21",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "96c132c7f2f7bc3230723b66e89f8f150b29d5ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/96c132c7f2f7bc3230723b66e89f8f150b29d5ae",
"reference": "96c132c7f2f7bc3230723b66e89f8f150b29d5ae",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2022-02-16T17:07:03+00:00"
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.43",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "709ec107af3cb2f385b9617be72af8cf62441d02"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02",
"reference": "709ec107af3cb2f385b9617be72af8cf62441d02",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1|^2|^3",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": ">=5.6.1"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"suggest": {
"ext-dom": "Install the DOM extension to load XML formatted public keys.",
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
},
"type": "library",
"autoload": {
"files": [
"phpseclib/bootstrap.php"
],
"psr-4": {
"phpseclib3\\": "phpseclib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jim Wigginton",
"email": "terrafrost@php.net",
"role": "Lead Developer"
},
{
"name": "Patrick Monnerat",
"email": "pm@datasphere.ch",
"role": "Developer"
},
{
"name": "Andreas Fischer",
"email": "bantu@phpbb.com",
"role": "Developer"
},
{
"name": "Hans-Jürgen Petrich",
"email": "petrich@tronic-media.com",
"role": "Developer"
},
{
"name": "Graham Campbell",
"email": "graham@alt-three.com",
"role": "Developer"
}
],
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
"homepage": "http://phpseclib.sourceforge.net",
"keywords": [
"BigInteger",
"aes",
"asn.1",
"asn1",
"blowfish",
"crypto",
"cryptography",
"encryption",
"rsa",
"security",
"sftp",
"signature",
"signing",
"ssh",
"twofish",
"x.509",
"x509"
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.43"
},
"funding": [
{
"url": "https://github.com/terrafrost",
"type": "github"
},
{
"url": "https://www.patreon.com/phpseclib",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
"type": "tidelift"
}
],
"time": "2024-12-14T21:12:59+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.6"
},
"platform-dev": [],
"plugin-api-version": "2.2.0"
}

15
dev/php506/Makefile Normal file
View file

@ -0,0 +1,15 @@
PHP_506 := $(shell which php5.6)
COMPOSER_LTS := $(shell which composer-lts)
VENDOR_BIN := $(realpath vendor/bin)
PHPUNIT = "${VENDOR_BIN}/phpunit"
install:
${PHP_506} ${COMPOSER_LTS} install
update:
${PHP_506} ${COMPOSER_LTS} update
analyze: analyse
analyse:
echo No static analysis available for PHP 5.6.

5
dev/php506/composer.json Normal file
View file

@ -0,0 +1,5 @@
{
"require-dev": {
"phpunit/phpunit": "~5.7"
}
}

1550
dev/php506/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

15
dev/php700/Makefile Normal file
View file

@ -0,0 +1,15 @@
PHP_700 := $(shell which php7.0)
COMPOSER_LTS := $(shell which composer-lts)
VENDOR_BIN := $(realpath vendor/bin)
PHPUNIT = "${VENDOR_BIN}/phpunit"
install:
${PHP_700} ${COMPOSER_LTS} install
update:
${PHP_700} ${COMPOSER_LTS} update
analyze: analyse
analyse:
echo No static analysis available for PHP 7.0.

5
dev/php700/composer.json Normal file
View file

@ -0,0 +1,5 @@
{
"require-dev": {
"phpunit/phpunit": "~6.5"
}
}

1712
dev/php700/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php701/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_701 := $(shell which php7.1)
COMPOSER_LTS := $(shell which composer-lts)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_LEGACY_CONFIG),undefined)
$(error PHPSTAN_LEGACY_CONFIG is not defined)
endif
install:
${PHP_701} ${COMPOSER_LTS} install
update:
${PHP_701} ${COMPOSER_LTS} update
analyze: analyse
analyse:
${PHP_701} ${PHPSTAN} analyze -c ${PHPSTAN_LEGACY_CONFIG}

6
dev/php701/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^1.4",
"phpunit/phpunit": "~7.5"
}
}

1763
dev/php701/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php702/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_702 := $(shell which php7.2)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_702} ${COMPOSER} install
update:
${PHP_702} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_702} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php702/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "~8.5"
}
}

1506
dev/php702/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php703/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_703 := $(shell which php7.3)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_703} ${COMPOSER} install
update:
${PHP_703} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_703} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php703/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "~9.6"
}
}

1824
dev/php703/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php704/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_704 := $(shell which php7.4)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_704} ${COMPOSER} install
update:
${PHP_704} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_704} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php704/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~9.6"
}
}

1826
dev/php704/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php800/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_800 := $(shell which php8.0)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_800} ${COMPOSER} install
update:
${PHP_800} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_800} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php800/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~9.6"
}
}

1826
dev/php800/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php801/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_801 := $(shell which php8.1)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_801} ${COMPOSER} install
update:
${PHP_801} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_801} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php801/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~10.5"
}
}

1709
dev/php801/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php802/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_802 := $(shell which php8.2)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_802} ${COMPOSER} install
update:
${PHP_802} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_802} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php802/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~11.5"
}
}

1773
dev/php802/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php803/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_803 := $(shell which php8.3)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_803} ${COMPOSER} install
update:
${PHP_803} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_803} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php803/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~12.1"
}
}

1667
dev/php803/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

20
dev/php804/Makefile Normal file
View file

@ -0,0 +1,20 @@
PHP_804 := $(shell which php8.4)
COMPOSER := $(shell which composer)
VENDOR_BIN := $(realpath vendor/bin)
PHPSTAN = "${VENDOR_BIN}/phpstan"
PHPUNIT = "${VENDOR_BIN}/phpunit"
ifeq ($(origin PHPSTAN_CONFIG),undefined)
$(error PHPSTAN_CONFIG is not defined)
endif
install:
${PHP_804} ${COMPOSER} install
update:
${PHP_804} ${COMPOSER} update
analyze: analyse
analyse:
${PHP_804} ${PHPSTAN} analyze -c ${PHPSTAN_CONFIG}

6
dev/php804/composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~12.1"
}
}

1667
dev/php804/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

9
phpstan-legacy.neon Normal file
View file

@ -0,0 +1,9 @@
parameters:
level: 9
checkUninitializedProperties: true
treatPhpDocTypesAsCertain: false
reportUnmatchedIgnoredErrors: false
paths:
- src
bootstrapFiles:
- vendor/autoload.php

11
phpstan.neon Normal file
View file

@ -0,0 +1,11 @@
parameters:
level: 9
checkUninitializedProperties: true
checkImplicitMixed: true
checkBenevolentUnionTypes: true
treatPhpDocTypesAsCertain: false
reportUnmatchedIgnoredErrors: false
paths:
- src
bootstrapFiles:
- vendor/autoload.php

72
src/ArrayJwkSet.php Normal file
View file

@ -0,0 +1,72 @@
<?php
namespace Railgun\Jwt;
use RuntimeException;
use InvalidArgumentException;
/**
* Represents a JSON Web Key Set backed by an array.
*/
class ArrayJwkSet implements JwkSet {
/**
* @var array<string, Jwk>
*/
private $keys;
/**
* @param array<string, Jwk> $keys
* @throws InvalidArgumentException
*/
public function __construct($keys) {
if(!is_array($keys))
throw new InvalidArgumentException('$keys must be an array');
if(!array_all($keys, [self::class, 'ensureArrayType']))
throw new InvalidArgumentException('$keys does not conform array<string, Jwk>');
$this->keys = $keys;
}
/**
* @param mixed $value
* @param mixed $key
* @return bool
*/
private static function ensureArrayType($value, $key) {
return !is_string($key) || !($value instanceof Jwk);
}
public function getKeys() {
return $this->keys;
}
public function getKey($keyId = null, $algo = null) {
if($keyId !== null && !is_string($keyId))
throw new InvalidArgumentException('$keyId must be a string or null');
if($algo !== null && !is_string($algo))
throw new InvalidArgumentException('$algo must be a string or null');
if($keyId === null) {
if($algo === null)
return $this->keys[array_key_first($this->keys)];
foreach($this->keys as $key) {
$keyAlgo = $key->getAlgorithm();
if($keyAlgo !== null && hash_equals($keyAlgo, $algo))
return $key;
}
throw new RuntimeException('could not find a key that matched the requested algorithm');
}
if(!array_key_exists($keyId, $this->keys))
throw new RuntimeException('could not find a key with that id');
$key = $this->keys[$keyId];
$keyAlgo = $key->getAlgorithm();
if($algo !== null && $keyAlgo !== null && !hash_equals($keyAlgo, $algo))
throw new RuntimeException('requested algorithm does not match');
return $key;
}
}

124
src/HmacJwk.php Normal file
View file

@ -0,0 +1,124 @@
<?php
namespace Railgun\Jwt;
use RuntimeException;
use InvalidArgumentException;
/**
* Represents a HMAC JSON Web Key.
*/
class HmacJwk implements Jwk {
/**
* @var array<string, string>
*/
private static $algos = [];
/**
* @var string
*/
private $key;
/**
* @var ?string
*/
private $algo;
/**
* @var ?string
*/
private $id;
/**
* @param string $key
* @param ?string $algo
* @param ?string $id
*/
public function __construct(
#[\SensitiveParameter]
$key,
$algo = null,
$id = null
) {
if(!is_string($key))
throw new InvalidArgumentException('$key must be a string');
if($algo !== null && !is_string($algo))
throw new InvalidArgumentException('$algo must be a string or null');
if($id !== null && !is_string($id))
throw new InvalidArgumentException('$id must be a string or null');
$this->key = $key;
$this->algo = $algo;
$this->id = $id;
}
public function getId() {
return $this->id;
}
public function getAlgorithm() {
return $this->algo;
}
public function sign($data, $algo = null) {
if(!is_string($data))
throw new InvalidArgumentException('$data must be a string');
if($algo !== null && !is_string($algo))
throw new InvalidArgumentException('$algo must be a string or null');
if($algo === null) {
if($this->algo === null)
throw new InvalidArgumentException('this key does not specify a default algorithm, you must specify $algo');
$algo = $this->algo;
}
if(!array_key_exists($algo, self::$algos))
throw new InvalidArgumentException('$algo is not a supported algorithm');
$result = hash_hmac(self::$algos[$algo], $data, $this->key, true);
if($result === false) // @phpstan-ignore identical.alwaysFalse
throw new RuntimeException('failed to sign $data');
return $result;
}
public function verify($data, $signature, $algo = null) {
if(!is_string($signature))
throw new InvalidArgumentException('$signature must be a string');
return hash_equals($this->sign($data, $algo), $signature);
}
/**
* Registered JWK aliases of native hashing algorithms.
*
* @return string[]
*/
public static function algorithms() {
return array_keys(self::$algos);
}
/**
* Defines an algorithm alias.
*
* @param string $alias JWK algorithm.
* @param string $algo Native PHP algorithm.
* @return void
*/
public static function defineAlgorithm($alias, $algo) {
if(!is_string($alias))
throw new InvalidArgumentException('$alias must be a string');
if(!is_string($algo))
throw new InvalidArgumentException('$algo must be a string');
if(array_key_exists($alias, self::$algos))
throw new InvalidArgumentException('$alias has already been defined');
if(!in_array($algo, hash_hmac_algos()))
throw new InvalidArgumentException('$algo is not a supported HMAC hashing algorithm');
self::$algos[$alias] = $algo;
}
}
HmacJwk::defineAlgorithm('HS256', 'sha256');
HmacJwk::defineAlgorithm('HS384', 'sha384');
HmacJwk::defineAlgorithm('HS512', 'sha512');

38
src/Jwk.php Normal file
View file

@ -0,0 +1,38 @@
<?php
namespace Railgun\Jwt;
use InvalidArgumentException;
use RuntimeException;
/**
* Represents a JSON Web Key.
*/
interface Jwk {
/**
* @return ?string
*/
public function getId();
/**
* @return ?string
*/
public function getAlgorithm();
/**
* @param string $data
* @param ?string $algo
* @throws RuntimeException
* @throws InvalidArgumentException
* @return string
*/
public function sign($data, $algo = null);
/**
* @param string $data
* @param string $signature
* @param ?string $algo
* @throws InvalidArgumentException
* @return bool
*/
public function verify($data, $signature, $algo = null);
}

24
src/JwkSet.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace Railgun\Jwt;
use InvalidArgumentException;
use RuntimeException;
/**
* Represents a JSON Web Key Set.
*/
interface JwkSet {
/**
* @return array<string, Jwk>
*/
public function getKeys();
/**
* @param ?string $keyId
* @param ?string $algo
* @throws RuntimeException
* @throws InvalidArgumentException
* @return Jwk
*/
public function getKey($keyId = null, $algo = null);
}

128
src/SecLibJwk.php Normal file
View file

@ -0,0 +1,128 @@
<?php
namespace Railgun\Jwt;
use InvalidArgumentException;
use RuntimeException;
use Stringable;
use UnexpectedValueException;
use phpseclib3\Crypt\Common\{AsymmetricKey,PrivateKey,PublicKey};
/**
* Implements a PHP Sec Lib backed JWK.
*/
class SecLibJwk implements Jwk {
/**
* @var (AsymmetricKey&PrivateKey)|(AsymmetricKey&PublicKey)
*/
private $key;
/**
* @var ?string
*/
private $id;
/**
* @var ?SecLibJwkAlgorithm
*/
private $algoInfo;
/**
* @param (AsymmetricKey&PrivateKey)|(AsymmetricKey&PublicKey) $key
* @param SecLibJwkAlgorithm|string|null $algo
* @param ?string $id
*/
public function __construct(
#[\SensitiveParameter]
$key,
$algo = null,
$id = null
) {
if(!($key instanceof AsymmetricKey) || (!($key instanceof PublicKey) && !($key instanceof PrivateKey)))
throw new InvalidArgumentException('$key is not a supported type combination');
if($id !== null && !is_string($id))
throw new InvalidArgumentException('$id must be a string or null');
if($algo !== null) {
if(is_string($algo))
$algo = SecLibJwkAlgorithm::create($algo);
elseif(!($algo instanceof SecLibJwkAlgorithm))
throw new InvalidArgumentException('$algo must be a SecLibJwkAlgorithm, string or null');
}
$this->key = $key;
$this->algoInfo = $algo;
$this->id = $id;
}
public function getId() {
return $this->id;
}
public function getAlgorithm() {
return $this->algoInfo instanceof SecLibJwkAlgorithm
? $this->algoInfo->getAlgorithm()
: null;
}
/**
* @param SecLibJwkAlgorithm|string|null $algo
* @throws InvalidArgumentException
* @return SecLibJwkAlgorithm
*/
private function resolveAlgo($algo) {
if($algo === null) {
if($this->algoInfo === null)
throw new InvalidArgumentException('this key does not specify a default algorithm, you must specify $algo');
$algo = $this->algoInfo;
} elseif(is_string($algo))
$algo = SecLibJwkAlgorithm::create($algo);
if($algo instanceof SecLibJwkAlgorithm)
return $algo;
throw new InvalidArgumentException('$algo could not be resolved');
}
/**
* {@inheritdoc}
* @param SecLibJwkAlgorithm|string|null $algo
* @throws UnexpectedValueException
*/
public function sign($data, $algo = null) {
if(!is_string($data))
throw new InvalidArgumentException('$data must be a string');
$key = $this->resolveAlgo($algo)->transform($this->key);
if(!($key instanceof PrivateKey))
throw new RuntimeException('this instance does not have a private key, it can only be used to verify');
$signature = $key->sign($data);
if(is_scalar($signature) || $signature instanceof Stringable)
return (string)$signature;
throw new UnexpectedValueException('unexpected return value for signature');
}
/**
* {@inheritdoc}
* @param SecLibJwkAlgorithm|string|null $algo
*/
public function verify($data, $signature, $algo = null) {
if(!is_string($data))
throw new InvalidArgumentException('$data must be a string');
if(!is_string($signature))
throw new InvalidArgumentException('$signature must be a string');
$key = $this->resolveAlgo($algo)->transform($this->key);
if($key instanceof PrivateKey)
$key = $key->getPublicKey();
if(!($key instanceof PublicKey))
throw new UnexpectedValueException('key result is not of expected type');
$result = $key->verify($data, $signature);
if(is_scalar($result))
return (bool)$result;
throw new UnexpectedValueException('unexpected return value for verification');
}
}

354
src/SecLibJwkAlgorithm.php Normal file
View file

@ -0,0 +1,354 @@
<?php
namespace Railgun\Jwt;
use InvalidArgumentException;
use RuntimeException;
use UnexpectedValueException;
use phpseclib3\Crypt\{EC,RSA};
use phpseclib3\Crypt\Common\AsymmetricKey;
/**
* Implements a PHP Sec Lib backed JWK Algorithm.
*/
class SecLibJwkAlgorithm {
/**
* @var array<string, callable(AsymmetricKey): AsymmetricKey>
*/
private static $algos = [];
/**
* @var string
*/
private $algo;
/**
* @var callable(AsymmetricKey): AsymmetricKey
*/
public $transformFunc;
/**
* @param string $algo
* @param callable(AsymmetricKey): AsymmetricKey $transformFunc
*/
public function __construct($algo, $transformFunc) {
if(!is_string($algo))
throw new InvalidArgumentException('$algo must be a string');
if(!is_callable($transformFunc))
throw new InvalidArgumentException('$transformFunc must be callable');
$this->algo = $algo;
$this->transformFunc = $transformFunc;
}
/**
* @return string
*/
public function getAlgorithm() {
return $this->algo;
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @throws UnexpectedValueException
* @return AsymmetricKey
*/
public function transform($key) {
if(!($key instanceof AsymmetricKey))
throw new InvalidArgumentException('$key must be an instance of AsymmetricKey');
$key = ($this->transformFunc)($key);
if(!($key instanceof AsymmetricKey))
throw new UnexpectedValueException('result of the transformer was not an AsymmetricKey');
return $key;
}
/**
* @param string $algo
* @throws InvalidArgumentException
* @return SecLibJwkAlgorithm
*/
public static function create($algo) {
if(!is_string($algo))
throw new InvalidArgumentException('$algo must be a string');
if(!array_key_exists($algo, self::$algos))
throw new InvalidArgumentException(sprintf('$algo is not a supported algorithm: "%s"', $algo));
return new SecLibJwkAlgorithm($algo, self::$algos[$algo]);
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @return RSA
*/
public static function ensureRsaKey($key) {
if($key instanceof RSA)
return $key;
throw new InvalidArgumentException('$key must be an RSA key');
}
/**
* @param RSA $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function applyPkcs1Padding($key) {
$key = $key->withPadding(RSA::SIGNATURE_PKCS1);
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param RSA $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function applyPssPadding($key) {
$key = $key->withPadding(RSA::SIGNATURE_PSS);
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @return EC
*/
public static function ensureEcKey($key) {
if($key instanceof EC)
return $key;
throw new InvalidArgumentException('$key must be an EC key');
}
/**
* @param EC $key
* @throws UnexpectedValueException
* @return EC
*/
public static function applyIeeeSignatureFormat($key) {
$key = $key->withSignatureFormat('IEEE');
if(!($key instanceof EC))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @return string[]
*/
public static function algorithms() {
return array_keys(self::$algos);
}
/**
* @param AsymmetricKey $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function transformRs256($key) {
$key = self::applyPkcs1Padding(self::ensureRsaKey($key))->withHash('sha256');
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function transformRs384($key) {
$key = self::applyPkcs1Padding(self::ensureRsaKey($key))->withHash('sha384');
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function transformRs512($key) {
$key = self::applyPkcs1Padding(self::ensureRsaKey($key))->withHash('sha512');
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function transformPs256($key) {
$key = self::applyPssPadding(self::ensureRsaKey($key))->withHash('sha256');
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function transformPs384($key) {
$key = self::applyPssPadding(self::ensureRsaKey($key))->withHash('sha384');
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws UnexpectedValueException
* @return RSA
*/
public static function transformPs512($key) {
$key = self::applyPssPadding(self::ensureRsaKey($key))->withHash('sha512');
if(!($key instanceof RSA))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @throws UnexpectedValueException
* @return EC
*/
public static function transformEs256($key) {
$key = self::ensureEcKey($key);
if($key->getCurve() !== 'secp256r1')
throw new InvalidArgumentException('curve must be secp256r1');
$key = self::applyIeeeSignatureFormat($key)->withHash('sha256');
if(!($key instanceof EC))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @throws UnexpectedValueException
* @return EC
*/
public static function transformEs256K($key) {
$key = self::ensureEcKey($key);
if($key->getCurve() !== 'secp256k1')
throw new InvalidArgumentException('curve must be secp256k1');
$key = self::applyIeeeSignatureFormat($key)->withHash('sha256');
if(!($key instanceof EC))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @throws UnexpectedValueException
* @return EC
*/
public static function transformEs384($key) {
$key = self::ensureEcKey($key);
if($key->getCurve() !== 'secp384r1')
throw new InvalidArgumentException('curve must be secp384r1');
$key = self::applyIeeeSignatureFormat($key)->withHash('sha384');
if(!($key instanceof EC))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @throws UnexpectedValueException
* @return EC
*/
public static function transformEs512($key) {
$key = self::ensureEcKey($key);
if($key->getCurve() !== 'secp521r1')
throw new InvalidArgumentException('curve must be secp521r1');
$key = self::applyIeeeSignatureFormat($key)->withHash('sha512');
if(!($key instanceof EC))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param AsymmetricKey $key
* @throws InvalidArgumentException
* @throws UnexpectedValueException
* @return EC
*/
public static function transformEdDsa($key) {
$key = self::ensureEcKey($key);
if($key->getCurve() !== 'Ed25519' && $key->getCurve() !== 'Ed448')
throw new InvalidArgumentException('curve must be Ed25519 or Ed448');
$key = $key;
if(!($key instanceof EC))
throw new UnexpectedValueException('return type changed unexpectedly');
return $key;
}
/**
* @param string $algo
* @param callable(AsymmetricKey): AsymmetricKey $transformFunc
* @throws InvalidArgumentException
* @return void
*/
public static function defineAlgorithm($algo, $transformFunc) {
if(!is_string($algo))
throw new InvalidArgumentException('$algo must be a string');
if(!is_callable($transformFunc))
throw new InvalidArgumentException('$transformFunc must be a callable');
if(array_key_exists($algo, self::$algos))
throw new InvalidArgumentException('$algo has already been defined');
self::$algos[$algo] = $transformFunc;
}
}
// RSxxx
SecLibJwkAlgorithm::defineAlgorithm('RS256', [SecLibJwkAlgorithm::class, 'transformRS256']);
SecLibJwkAlgorithm::defineAlgorithm('RS384', [SecLibJwkAlgorithm::class, 'transformRS384']);
SecLibJwkAlgorithm::defineAlgorithm('RS512', [SecLibJwkAlgorithm::class, 'transformRS512']);
// PSxxx
SecLibJwkAlgorithm::defineAlgorithm('PS256', [SecLibJwkAlgorithm::class, 'transformPS256']);
SecLibJwkAlgorithm::defineAlgorithm('PS384', [SecLibJwkAlgorithm::class, 'transformPS384']);
SecLibJwkAlgorithm::defineAlgorithm('PS512', [SecLibJwkAlgorithm::class, 'transformPS512']);
// ESxxxy
SecLibJwkAlgorithm::defineAlgorithm('ES256', [SecLibJwkAlgorithm::class, 'transformES256']);
SecLibJwkAlgorithm::defineAlgorithm('ES256K', [SecLibJwkAlgorithm::class, 'transformES256K']);
SecLibJwkAlgorithm::defineAlgorithm('ES384', [SecLibJwkAlgorithm::class, 'transformES384']);
SecLibJwkAlgorithm::defineAlgorithm('ES512', [SecLibJwkAlgorithm::class, 'transformES512']);
// EdDSA
SecLibJwkAlgorithm::defineAlgorithm('EdDSA', [SecLibJwkAlgorithm::class, 'transformEdDSA']);

55
src/polyfill.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace Railgun\Jwt;
use InvalidArgumentException;
if(!function_exists('array_key_first')) {
/**
* Gets the first key of an array.
*
* @param mixed[] $array An array.
* @return int|string|null Returns the first key of array if array is not empty; null otherwise.
*/
function array_key_first($array) {
if(!is_array($array))
throw new InvalidArgumentException('$array must be an array');
foreach($array as $key => $_)
return $key;
return null;
}
}
if(!function_exists('array_all')) {
/**
* Checks if all array elements satisfy a callback function.
*
* @param mixed[] $array The array that should be searched.
* @param callable(mixed $value, mixed $key): bool $callback The callback function to call to check each element.
* @return bool The function returns true, if callback returns true for all elements. Otherwise returns false.
*/
function array_all($array, $callback) {
if(!is_array($array))
throw new InvalidArgumentException('$array must be an array');
if(!is_callable($callback))
throw new InvalidArgumentException('$callback must be a callable');
foreach($array as $key => $value)
if(!$callback($value, $key))
return false;
return true;
}
}
if(!function_exists('hash_hmac_algos')) {
/**
* Return a list of registered hashing algorithms suitable for hash_hmac.
*
* @return string[] Returns a numerically indexed array containing the list of supported hashing algorithms suitable for hash_hmac().
*/
function hash_hmac_algos(): array {
return hash_algos();
}
}