diff --git a/src/network/mcpe/JwtUtils.php b/src/network/mcpe/JwtUtils.php index 358da0372..3e847f859 100644 --- a/src/network/mcpe/JwtUtils.php +++ b/src/network/mcpe/JwtUtils.php @@ -23,17 +23,39 @@ declare(strict_types=1); namespace pocketmine\network\mcpe; +use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface; +use Mdanter\Ecc\Crypto\Key\PublicKeyInterface; +use Mdanter\Ecc\Crypto\Signature\Signature; +use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer; +use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer; +use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; +use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer; +use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; +use pocketmine\network\mcpe\auth\VerifyLoginException; +use pocketmine\utils\AssumptionFailedError; use function base64_decode; use function base64_encode; +use function bin2hex; use function count; use function explode; +use function gmp_init; +use function gmp_strval; +use function hex2bin; use function is_array; use function json_decode; +use function json_encode; use function json_last_error_msg; +use function openssl_error_string; +use function openssl_sign; +use function openssl_verify; use function rtrim; +use function str_pad; use function str_repeat; +use function str_split; use function strlen; use function strtr; +use const OPENSSL_ALGO_SHA384; +use const STR_PAD_LEFT; final class JwtUtils{ @@ -62,6 +84,57 @@ final class JwtUtils{ return [$header, $body, $signature]; } + /** + * @throws \UnexpectedValueException + */ + public static function verify(string $jwt, PublicKeyInterface $signingKey) : bool{ + $parts = explode('.', $jwt); + if(count($parts) !== 3){ + throw new \UnexpectedValueException("Expected exactly 3 JWT parts, got " . count($parts)); + } + [$header, $body, $signature] = $parts; + + $plainSignature = self::b64UrlDecode($signature); + if(strlen($plainSignature) !== 96){ + throw new VerifyLoginException("JWT signature has unexpected length, expected 96, got " . strlen($plainSignature)); + } + + [$rString, $sString] = str_split($plainSignature, 48); + $sig = new Signature(gmp_init(bin2hex($rString), 16), gmp_init(bin2hex($sString), 16)); + + $v = openssl_verify( + $header . '.' . $body, + (new DerSignatureSerializer())->serialize($sig), + (new PemPublicKeySerializer(new DerPublicKeySerializer()))->serialize($signingKey), + OPENSSL_ALGO_SHA384 + ); + switch($v){ + case 0: return false; + case 1: return true; + case -1: throw new \UnexpectedValueException("Error verifying JWT signature: " . openssl_error_string()); + default: throw new AssumptionFailedError("openssl_verify() should only return -1, 0 or 1"); + } + } + + public static function create(array $header, array $claims, PrivateKeyInterface $signingKey) : string{ + $jwtBody = JwtUtils::b64UrlEncode(json_encode($header)) . "." . JwtUtils::b64UrlEncode(json_encode($claims)); + + openssl_sign( + $jwtBody, + $sig, + (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($signingKey), + OPENSSL_ALGO_SHA384 + ); + + $decodedSig = (new DerSignatureSerializer())->parse($sig); + $jwtSig = JwtUtils::b64UrlEncode( + hex2bin(str_pad(gmp_strval($decodedSig->getR(), 16), 96, "0", STR_PAD_LEFT)) . + hex2bin(str_pad(gmp_strval($decodedSig->getS(), 16), 96, "0", STR_PAD_LEFT)) + ); + + return "$jwtBody.$jwtSig"; + } + public static function b64UrlEncode(string $str) : string{ return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); } diff --git a/src/network/mcpe/auth/ProcessLoginTask.php b/src/network/mcpe/auth/ProcessLoginTask.php index 0e10d0e12..24b3bcbb6 100644 --- a/src/network/mcpe/auth/ProcessLoginTask.php +++ b/src/network/mcpe/auth/ProcessLoginTask.php @@ -25,24 +25,12 @@ namespace pocketmine\network\mcpe\auth; use FG\ASN1\Exception\ParserException; use Mdanter\Ecc\Crypto\Key\PublicKeyInterface; -use Mdanter\Ecc\Crypto\Signature\Signature; use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; -use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer; -use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\protocol\LoginPacket; use pocketmine\scheduler\AsyncTask; -use pocketmine\utils\AssumptionFailedError; use function base64_decode; -use function bin2hex; -use function count; -use function explode; -use function gmp_init; -use function openssl_verify; -use function str_split; -use function strlen; use function time; -use const OPENSSL_ALGO_SHA384; class ProcessLoginTask extends AsyncTask{ private const TLS_KEY_ON_COMPLETION = "completion"; @@ -117,7 +105,7 @@ class ProcessLoginTask extends AsyncTask{ */ private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{ try{ - [$headers, $claims, $plainSignature] = JwtUtils::parse($jwt); + [$headers, $claims, ] = JwtUtils::parse($jwt); }catch(\UnexpectedValueException $e){ throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), 0, $e); } @@ -131,13 +119,6 @@ class ProcessLoginTask extends AsyncTask{ $currentPublicKey = $headers["x5u"]; } - if(strlen($plainSignature) !== 96){ - throw new VerifyLoginException("JWT signature has unexpected length, expected 96, got " . strlen($plainSignature)); - } - - [$rString, $sString] = str_split($plainSignature, 48); - $sig = new Signature(gmp_init(bin2hex($rString), 16), gmp_init(bin2hex($sString), 16)); - $derPublicKeySerializer = new DerPublicKeySerializer(); $rawPublicKey = base64_decode($currentPublicKey, true); if($rawPublicKey === false){ @@ -149,17 +130,12 @@ class ProcessLoginTask extends AsyncTask{ throw new VerifyLoginException("Failed to parse DER public key: " . $e->getMessage(), 0, $e); } - $rawParts = explode('.', $jwt); - if(count($rawParts) !== 3) throw new AssumptionFailedError("Parts count should be 3 as verified by JwtUtils::parse()"); - $v = openssl_verify( - $rawParts[0] . '.' . $rawParts[1], - (new DerSignatureSerializer())->serialize($sig), - (new PemPublicKeySerializer($derPublicKeySerializer))->serialize($signingKey), - OPENSSL_ALGO_SHA384 - ); - - if($v !== 1){ - throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.badSignature"); + try{ + if(!JwtUtils::verify($jwt, $signingKey)){ + throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.badSignature"); + } + }catch(\UnexpectedValueException $e){ + throw new VerifyLoginException($e->getMessage(), 0, $e); } if($currentPublicKey === self::MOJANG_ROOT_PUBLIC_KEY){ diff --git a/src/network/mcpe/encryption/EncryptionUtils.php b/src/network/mcpe/encryption/EncryptionUtils.php index a396c8ab1..c34e4a38f 100644 --- a/src/network/mcpe/encryption/EncryptionUtils.php +++ b/src/network/mcpe/encryption/EncryptionUtils.php @@ -25,17 +25,12 @@ namespace pocketmine\network\mcpe\encryption; use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface; use Mdanter\Ecc\Crypto\Key\PublicKeyInterface; -use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer; -use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; -use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; use pocketmine\network\mcpe\JwtUtils; use function base64_encode; use function gmp_strval; use function hex2bin; -use function json_encode; use function openssl_digest; -use function openssl_sign; use function str_pad; final class EncryptionUtils{ @@ -53,23 +48,15 @@ final class EncryptionUtils{ } public static function generateServerHandshakeJwt(PrivateKeyInterface $serverPriv, string $salt) : string{ - $jwtBody = JwtUtils::b64UrlEncode(json_encode([ - "x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())), - "alg" => "ES384" - ]) - ) . "." . JwtUtils::b64UrlEncode(json_encode([ - "salt" => base64_encode($salt) - ]) - ); - - openssl_sign($jwtBody, $sig, (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($serverPriv), OPENSSL_ALGO_SHA384); - - $decodedSig = (new DerSignatureSerializer())->parse($sig); - $jwtSig = JwtUtils::b64UrlEncode( - hex2bin(str_pad(gmp_strval($decodedSig->getR(), 16), 96, "0", STR_PAD_LEFT)) . - hex2bin(str_pad(gmp_strval($decodedSig->getS(), 16), 96, "0", STR_PAD_LEFT)) + return JwtUtils::create( + [ + "x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())), + "alg" => "ES384" + ], + [ + "salt" => base64_encode($salt) + ], + $serverPriv ); - - return "$jwtBody.$jwtSig"; } }