Finished JWT/JWS implementation.

This commit is contained in:
flash 2024-07-16 22:15:01 +00:00
parent 9fdfa5caa2
commit dc363f1854
10 changed files with 1052 additions and 223 deletions

View file

@ -8,7 +8,8 @@
"symfony/mailer": "^6.0",
"matomo/device-detector": "^6.1",
"sentry/sdk": "^4.0",
"flashwave/syokuhou": "dev-master"
"flashwave/syokuhou": "dev-master",
"phpseclib/phpseclib": "~3.0"
},
"autoload": {
"classmap": [

364
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cb1b8aba5b1b619c5fd4d7187ac2018c",
"content-hash": "ce2957bfc51cf8af01d85ba4676d1655",
"packages": [
{
"name": "chillerlan/php-qrcode",
@ -89,16 +89,16 @@
},
{
"name": "chillerlan/php-settings-container",
"version": "3.2.0",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "8f93648fac8e6bacac8e00a8d325eba4950295e6"
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/8f93648fac8e6bacac8e00a8d325eba4950295e6",
"reference": "8f93648fac8e6bacac8e00a8d325eba4950295e6",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"shasum": ""
},
"require": {
@ -106,15 +106,16 @@
"php": "^8.1"
},
"require-dev": {
"phan/phan": "^5.4",
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.9"
"squizlabs/php_codesniffer": "^3.10"
},
"type": "library",
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src/"
"chillerlan\\Settings\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -150,7 +151,7 @@
"type": "ko_fi"
}
],
"time": "2024-03-02T20:07:15+00:00"
"time": "2024-07-16T11:13:48+00:00"
},
{
"name": "doctrine/lexer",
@ -720,6 +721,233 @@
},
"time": "2019-09-10T13:16:29+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
"phpunit/phpunit": "^9",
"vimeo/psalm": "^4|^5"
},
"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": "2024-05-08T12:36:18+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
"php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"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": "2020-10-15T08:29:30+00:00"
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.39",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "211ebc399c6e73c225a018435fe5ae209d1d1485"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/211ebc399c6e73c225a018435fe5ae209d1d1485",
"reference": "211ebc399c6e73c225a018435fe5ae209d1d1485",
"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.39"
},
"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-06-24T06:27:33+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
@ -1082,16 +1310,16 @@
},
{
"name": "sentry/sentry",
"version": "4.8.0",
"version": "4.8.1",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
"reference": "3cf5778ff425a23f2d22ed41b423691d36f47163"
"reference": "61770efd8b7888e0bdd7d234f0ba67b066e47d04"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/3cf5778ff425a23f2d22ed41b423691d36f47163",
"reference": "3cf5778ff425a23f2d22ed41b423691d36f47163",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/61770efd8b7888e0bdd7d234f0ba67b066e47d04",
"reference": "61770efd8b7888e0bdd7d234f0ba67b066e47d04",
"shasum": ""
},
"require": {
@ -1155,7 +1383,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
"source": "https://github.com/getsentry/sentry-php/tree/4.8.0"
"source": "https://github.com/getsentry/sentry-php/tree/4.8.1"
},
"funding": [
{
@ -1167,7 +1395,7 @@
"type": "custom"
}
],
"time": "2024-06-05T13:18:43+00:00"
"time": "2024-07-16T13:45:27+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -1394,16 +1622,16 @@
},
{
"name": "symfony/mailer",
"version": "v6.4.8",
"version": "v6.4.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "76326421d44c07f7824b19487cfbf87870b37efc"
"reference": "e2d56f180f5b8c5e7c0fbea872bb1f529b6d6d45"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/76326421d44c07f7824b19487cfbf87870b37efc",
"reference": "76326421d44c07f7824b19487cfbf87870b37efc",
"url": "https://api.github.com/repos/symfony/mailer/zipball/e2d56f180f5b8c5e7c0fbea872bb1f529b6d6d45",
"reference": "e2d56f180f5b8c5e7c0fbea872bb1f529b6d6d45",
"shasum": ""
},
"require": {
@ -1454,7 +1682,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v6.4.8"
"source": "https://github.com/symfony/mailer/tree/v6.4.9"
},
"funding": [
{
@ -1470,20 +1698,20 @@
"type": "tidelift"
}
],
"time": "2024-05-31T14:49:08+00:00"
"time": "2024-06-28T07:59:05+00:00"
},
{
"name": "symfony/mime",
"version": "v7.1.1",
"version": "v7.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "21027eaacc1a8a20f5e616c25c3580f5dd3a15df"
"reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/21027eaacc1a8a20f5e616c25c3580f5dd3a15df",
"reference": "21027eaacc1a8a20f5e616c25c3580f5dd3a15df",
"url": "https://api.github.com/repos/symfony/mime/zipball/26a00b85477e69a4bab63b66c5dce64f18b0cbfc",
"reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc",
"shasum": ""
},
"require": {
@ -1538,7 +1766,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.1.1"
"source": "https://github.com/symfony/mime/tree/v7.1.2"
},
"funding": [
{
@ -1554,7 +1782,7 @@
"type": "tidelift"
}
],
"time": "2024-06-04T06:40:14+00:00"
"time": "2024-06-28T10:03:55+00:00"
},
{
"name": "symfony/options-resolver",
@ -1625,16 +1853,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.29.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"shasum": ""
},
"require": {
@ -1684,7 +1912,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
},
"funding": [
{
@ -1700,20 +1928,20 @@
"type": "tidelift"
}
],
"time": "2024-01-29T20:11:03+00:00"
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.29.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "a287ed7475f85bf6f61890146edbc932c0fff919"
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919",
"reference": "a287ed7475f85bf6f61890146edbc932c0fff919",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
"shasum": ""
},
"require": {
@ -1768,7 +1996,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0"
},
"funding": [
{
@ -1784,20 +2012,20 @@
"type": "tidelift"
}
],
"time": "2024-01-29T20:11:03+00:00"
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.29.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
"reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"shasum": ""
},
"require": {
@ -1849,7 +2077,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
},
"funding": [
{
@ -1865,20 +2093,20 @@
"type": "tidelift"
}
],
"time": "2024-01-29T20:11:03+00:00"
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.29.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"shasum": ""
},
"require": {
@ -1929,7 +2157,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
},
"funding": [
{
@ -1945,20 +2173,20 @@
"type": "tidelift"
}
],
"time": "2024-01-29T20:11:03+00:00"
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.29.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25"
"reference": "10112722600777e02d2745716b70c5db4ca70442"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25",
"reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442",
"reference": "10112722600777e02d2745716b70c5db4ca70442",
"shasum": ""
},
"require": {
@ -2002,7 +2230,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0"
"source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0"
},
"funding": [
{
@ -2018,20 +2246,20 @@
"type": "tidelift"
}
],
"time": "2024-01-29T20:11:03+00:00"
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.29.0",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
"shasum": ""
},
"require": {
@ -2082,7 +2310,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
},
"funding": [
{
@ -2098,7 +2326,7 @@
"type": "tidelift"
}
],
"time": "2024-01-29T20:11:03+00:00"
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/service-contracts",
@ -2334,16 +2562,16 @@
"packages-dev": [
{
"name": "phpstan/phpstan",
"version": "1.11.4",
"version": "1.11.7",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82"
"reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9100a76ce8015b9aa7125b9171ae3a76887b6c82",
"reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/52d2bbfdcae7f895915629e4694e9497d0f8e28d",
"reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d",
"shasum": ""
},
"require": {
@ -2388,7 +2616,7 @@
"type": "github"
}
],
"time": "2024-06-06T12:19:22+00:00"
"time": "2024-07-06T11:17:41+00:00"
}
],
"aliases": [],

420
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
<?php
namespace Hanyuu\Jose;
class HmacJwsHandler implements IJwsHandler {
public const HS256 = 'HS256';
public const HS384 = 'HS384';
public const HS512 = 'HS512';
private const MAPPINGS = [
self::HS256 => 'sha256',
self::HS384 => 'sha384',
self::HS512 => 'sha512',
];
private string $realAlgo;
private $getSecret;
public function __construct(
private string $algo,
callable $getSecret
) {
if(!array_key_exists($algo, self::MAPPINGS))
throw new InvalidArgumentException('algorithm specified in $algo is not supported');
$this->realAlgo = self::MAPPINGS[$algo];
$this->getSecret = $getSecret;
}
public function getAlgorithm(): string {
return $this->algo;
}
public function match(object $header): bool {
return $header->alg === $this->algo;
}
public function sign(string $data): string {
return hash_hmac($this->realAlgo, $data, ($this->getSecret)(), true);
}
public function verify(string $data, string $userSignature): bool {
return hash_equals($this->sign($data), $userSignature);
}
}

9
src/Jose/IJwsHandler.php Normal file
View file

@ -0,0 +1,9 @@
<?php
namespace Hanyuu\Jose;
interface IJwsHandler {
function getAlgorithm(): string;
function match(object $header): bool; // alg is guaranteed to be present, you may make assumptions
function sign(string $data): string;
function verify(string $data, string $userSignature): bool;
}

69
src/Jose/Jws.php Normal file
View file

@ -0,0 +1,69 @@
<?php
namespace Hanyuu\Jose;
final class Jws {
private static function hmac(string $algo, callable|string $secret): IJwsHandler {
if(is_string($secret))
$secret = fn() => $secret;
return new HmacJwsHandler($algo, $secret);
}
public static function hs256(callable|string $secret): IJwsHandler {
return self::hmac(HmacJwsHandler::HS256, $secret);
}
public static function hs384(callable|string $secret): IJwsHandler {
return self::hmac(HmacJwsHandler::HS384, $secret);
}
public static function hs512(callable|string $secret): IJwsHandler {
return self::hmac(HmacJwsHandler::HS512, $secret);
}
private static function openSsl(string $algo, callable|string $privateKey, callable|string $publicKey): IJwsHandler {
if(is_string($privateKey))
$privateKey = fn() => $privateKey;
if(is_string($publicKey))
$publicKey = fn() => $publicKey;
return new OpenSslJwsHandler($algo, $privateKey, $publicKey);
}
public static function rs256(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::openSsl(OpenSslJwsHandler::RS256, $privateKey, $publicKey);
}
public static function rs384(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::openSsl(OpenSslJwsHandler::RS384, $privateKey, $publicKey);
}
public static function rs512(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::openSsl(OpenSslJwsHandler::RS512, $privateKey, $publicKey);
}
public static function es256(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::openSsl(OpenSslJwsHandler::ES256, $privateKey, $publicKey);
}
public static function es384(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::openSsl(OpenSslJwsHandler::ES384, $privateKey, $publicKey);
}
public static function es512(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::openSsl(OpenSslJwsHandler::ES512, $privateKey, $publicKey);
}
private static function phpSecLib(string $algo, callable|string $privateKey, callable|string $publicKey): IJwsHandler {
if(is_string($privateKey))
$privateKey = fn() => $privateKey;
if(is_string($publicKey))
$publicKey = fn() => $publicKey;
return new PhpSecLibJwsHandler($algo, $privateKey, $publicKey);
}
public static function ps256(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::phpSecLib(PhpSecLibJwsHandler::PS256, $privateKey, $publicKey);
}
public static function ps384(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::phpSecLib(PhpSecLibJwsHandler::PS384, $privateKey, $publicKey);
}
public static function ps512(callable|string $privateKey, callable|string $publicKey): IJwsHandler {
return self::phpSecLib(PhpSecLibJwsHandler::PS512, $privateKey, $publicKey);
}
}

44
src/Jose/JwsHandlers.php Normal file
View file

@ -0,0 +1,44 @@
<?php
namespace Hanyuu\Jose;
class JwsHandlers {
private array $handlers = [];
public function register(IJwsHandler $handler): void {
$this->handlers[] = $handler;
}
public function clear(): void {
$this->handlers = [];
}
public function getFirstAlgorithm(): ?IJwsHandler {
return empty($this->handlers) ? null : $this->handlers[0];
}
public function getOneByAlgorithm(string $name): ?IJwsHandler {
foreach($this->handlers as $handler)
if($handler->getAlgorithm() === $name)
return $handler;
return null;
}
public function match(object $header): array {
$handlers = [];
foreach($this->handlers as $handler)
if($handler->match($header))
$handlers[] = $handler;
return $handlers;
}
public function verify(object $header, string $data, string $userHash): bool {
foreach($this->handlers as $handler)
if($handler->match($header) && $handler->verify($data, $userHash))
return true;
return false;
}
}

139
src/Jose/JwtHandler.php Normal file
View file

@ -0,0 +1,139 @@
<?php
namespace Hanyuu\Jose;
use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Index\Serialisation\UriBase64;
class JwtHandler {
private JwsHandlers $jwsHandlers;
public function __construct() {
$this->jwsHandlers = new JwsHandlers;
}
public function getJwsHandlers(): JwsHandlers {
return $this->jwsHandlers;
}
public function registerJwsHandler(IJwsHandler $handler): void {
$this->jwsHandlers->register($handler);
}
public function encoded(
mixed $payload,
object|array|null $header = null,
bool $payloadToJson = true
): string {
if(!is_object($header)) {
if(is_array($header))
$header = (object)$header;
else
$header = new stdClass;
}
$header->alg = 'none';
$header = UriBase64::encode(json_encode($header, JSON_UNESCAPED_UNICODE));
$payload = UriBase64::encode($payloadToJson ? json_encode($payload, JSON_UNESCAPED_UNICODE) : (string)$payload);
return "{$header}.{$payload}.";
}
public function signed(
mixed $payload,
object|array|null $header = null,
bool $payloadToJson = true,
string|IJwsHandler|null $handler = null
): string {
if(!is_object($header)) {
if(is_array($header))
$header = (object)$header;
else
$header = new stdClass;
}
if($handler === null)
$handler = $this->jwsHandlers->getFirstAlgorithm();
elseif(is_string($handler))
$handler = $this->jwsHandlers->getOneByAlgorithm($handler);
if($handler === null)
throw new InvalidArgumentException('no JWS handler is associated with the algorithm specified in $handler');
$header->alg = $handler->getAlgorithm();
$header->typ = 'JWT';
$header = UriBase64::encode(json_encode($header, JSON_UNESCAPED_UNICODE));
$payload = UriBase64::encode($payloadToJson ? json_encode($payload, JSON_UNESCAPED_UNICODE) : (string)$payload);
$token = "{$header}.{$payload}";
$signature = UriBase64::encode($handler->sign($token));
return "{$token}.{$signature}";
}
public function decodeStrict(string $token, bool $verify = true): object {
if(strpos($token, '.') === false)
throw new InvalidArgumentException('$token is not a JWT');
$parts = explode('.', $token);
foreach($parts as $key => $value)
$parts[$key] = trim($value);
$header = json_decode(UriBase64::decode($parts[0]));
if(!is_object($header))
throw new InvalidArgumentException('unable to decode header field of $token');
if(!isset($header->alg) || !is_string($header->alg))
throw new InvalidArgumentException('alg claim missing from header in $token');
$tokenParts = count($parts);
if($tokenParts === 5)
$type = 'enc';
elseif($tokenParts === 3)
$type = $parts[2] === '' ? 'raw' : 'sig';
elseif($tokenParts === 2) // should this be supported? the spec is at least a little clear about the trailing dot
$type = 'raw';
else
throw new InvalidArgumentException('$token does not contain a supported amount of segments');
$info = new stdClass;
$info->success = true;
$info->type = $type;
$info->header = $header;
if($type === 'sig') {
if($verify && !$this->jwsHandlers->verify($header, "{$parts[0]}.{$parts[1]}", UriBase64::decode($parts[2])))
throw new InvalidArgumentException('could not verify signature of $token');
$info->payload = UriBase64::decode($parts[1]);
} elseif($type === 'enc') {
$info->success = false;
} elseif($type === 'raw') {
if($verify && $header->alg !== 'none')
throw new InvalidArgumentException('format of $token indicates that it should be an unsecured JWT, but "alg" is not set to "none"');
$info->payload = UriBase64::decode($parts[1]);
}
if(isset($info->payload)) {
$decoded = json_decode($info->payload);
if($decoded !== null)
$info->payload = $decoded;
}
return $info;
}
public function decode(string $token, bool $verify = true): object {
try {
return $this->decodeStrict($token, $verify);
} catch(InvalidArgumentException $ex) {
$info = new stdClass;
$info->success = false;
$info->message = $ex->getMessage();
return $info;
}
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Hanyuu\Jose;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use RuntimeException;
class OpenSslJwsHandler implements IJwsHandler {
public const RS256 = 'RS256';
public const RS384 = 'RS384';
public const RS512 = 'RS512';
public const ES256 = 'ES256';
public const ES384 = 'ES384';
public const ES512 = 'ES512';
private const OPENSSL_HASH_ALGO = [
self::RS256 => OPENSSL_ALGO_SHA256,
self::RS384 => OPENSSL_ALGO_SHA384,
self::RS512 => OPENSSL_ALGO_SHA512,
self::ES256 => OPENSSL_ALGO_SHA256,
self::ES384 => OPENSSL_ALGO_SHA384,
self::ES512 => OPENSSL_ALGO_SHA512,
];
private const OPENSSL_KEY_INFO = [
self::RS256 => ['type' => OPENSSL_KEYTYPE_RSA],
self::RS384 => ['type' => OPENSSL_KEYTYPE_RSA],
self::RS512 => ['type' => OPENSSL_KEYTYPE_RSA],
self::ES256 => ['type' => OPENSSL_KEYTYPE_EC, 'bits' => 256],
self::ES384 => ['type' => OPENSSL_KEYTYPE_EC, 'bits' => 384],
self::ES512 => ['type' => OPENSSL_KEYTYPE_EC, 'bits' => 521],
];
private $opensslHashAlgo;
private $getPrivateKey;
private $getPublicKey;
public function __construct(
private string $algo,
?callable $getPrivateKey,
?callable $getPublicKey
) {
if(!array_key_exists($algo, self::OPENSSL_HASH_ALGO))
throw new InvalidArgumentException('algorithm specified in $algo is not supported');
$this->opensslHashAlgo = self::OPENSSL_HASH_ALGO[$algo];
$this->getPrivateKey = $getPrivateKey;
$this->getPublicKey = $getPublicKey;
}
public function getAlgorithm(): string {
return $this->algo;
}
public function match(object $header): bool {
return $header->alg === $this->algo;
}
private function ensureValidKey(OpenSSLAsymmetricKey $key): void {
if(!array_key_exists($this->algo, self::OPENSSL_KEY_INFO))
throw new RuntimeException('missing key requirements for selected algorithm');
$require = self::OPENSSL_KEY_INFO[$this->algo];
$details = openssl_pkey_get_details($key);
foreach($require as $key => $value)
if($details[$key] !== $value)
throw new RuntimeException('provided asymmetric key is not suitable for this algorithm');
}
public function sign(string $data): string {
$privateKey = $this->getPrivateKey;
if($privateKey !== null)
$privateKey = ($privateKey)();
if($privateKey === null)
throw new RuntimeException('no private key available for signing');
if(is_string($privateKey))
$privateKey = openssl_pkey_get_private($privateKey);
$this->ensureValidKey($privateKey);
if(!openssl_sign($data, $signature, $privateKey, $this->opensslHashAlgo))
throw new RuntimeException((string)openssl_error_string());
return $signature;
}
public function verify(string $data, string $userSignature): bool {
$publicKey = $this->getPublicKey;
if($publicKey !== null)
$publicKey = ($publicKey)();
if($publicKey === null)
throw new RuntimeException('no public key available for verification');
if(is_string($publicKey))
$publicKey = openssl_pkey_get_public($publicKey);
$this->ensureValidKey($publicKey);
$result = openssl_verify($data, $userSignature, $publicKey, $this->opensslHashAlgo);
if($result === 1)
return true;
if($result === 0)
return false;
throw new RuntimeException((string)openssl_error_string());
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Hanyuu\Jose;
use InvalidArgumentException;
use RuntimeException;
use phpseclib3\Crypt\RSA;
class PhpSecLibJwsHandler implements IJwsHandler {
public const PS256 = 'PS256';
public const PS384 = 'PS384';
public const PS512 = 'PS512';
private const PHPSECLIB_HASH_ALGOS = [
self::PS256 => 'sha256',
self::PS384 => 'sha384',
self::PS512 => 'sha512',
];
private $phpSecLibHashAlgo;
private $getPrivateKey;
private $getPublicKey;
public function __construct(
private string $algo,
?callable $getPrivateKey,
?callable $getPublicKey
) {
if(!array_key_exists($algo, self::PHPSECLIB_HASH_ALGOS))
throw new InvalidArgumentException('algorithm specified in $algo is not supported');
$this->phpSecLibHashAlgo = self::PHPSECLIB_HASH_ALGOS[$algo];
$this->getPrivateKey = $getPrivateKey;
$this->getPublicKey = $getPublicKey;
}
public function getAlgorithm(): string {
return $this->algo;
}
public function match(object $header): bool {
return $header->alg === $this->algo;
}
public function sign(string $data): string {
$privateKey = $this->getPrivateKey;
if($privateKey !== null)
$privateKey = ($privateKey)();
if($privateKey === null)
throw new RuntimeException('no private key available for signing');
$privateKey = RSA::load($privateKey);
$privateKey->withPadding(RSA::SIGNATURE_PSS);
$privateKey->withHash($this->phpSecLibHashAlgo);
$privateKey->withMGFHash($this->phpSecLibHashAlgo);
return $privateKey->sign($data);
}
public function verify(string $data, string $userSignature): bool {
$publicKey = $this->getPublicKey;
if($publicKey !== null)
$publicKey = ($publicKey)();
if($publicKey === null)
throw new RuntimeException('no public key available for verification');
$publicKey = RSA::load($publicKey);
$publicKey->withPadding(RSA::SIGNATURE_PSS);
$publicKey->withHash($this->phpSecLibHashAlgo);
$publicKey->withMGFHash($this->phpSecLibHashAlgo);
return $publicKey->verify($data, $userSignature);
}
}