storeLocal($player); $this->packet = $packet; } public function onRun() : void{ $packet = $this->packet; //Get it in a local variable to make sure it stays unserialized try{ $currentKey = null; $first = true; foreach($packet->chainData["chain"] as $jwt){ $this->validateToken($jwt, $currentKey, $first); $first = false; } $this->validateToken($packet->clientDataJwt, $currentKey); $this->error = null; }catch(VerifyLoginException $e){ $this->error = $e->getMessage(); } } /** * @param string $jwt * @param null|string $currentPublicKey * @param bool $first * * @throws VerifyLoginException if errors are encountered */ private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{ [$headB64, $payloadB64, $sigB64] = explode('.', $jwt); $headers = json_decode(base64_decode(strtr($headB64, '-_', '+/'), true), true); if($currentPublicKey === null){ if(!$first){ throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.missingKey"); } //First link, check that it is self-signed $currentPublicKey = $headers["x5u"]; } $plainSignature = base64_decode(strtr($sigB64, '-_', '+/'), true); //OpenSSL wants a DER-encoded signature, so we extract R and S from the plain signature and crudely serialize it. assert(strlen($plainSignature) === 96); [$rString, $sString] = str_split($plainSignature, 48); $rString = ltrim($rString, "\x00"); if(ord($rString{0}) >= 128){ //Would be considered signed, pad it with an extra zero $rString = "\x00" . $rString; } $sString = ltrim($sString, "\x00"); if(ord($sString{0}) >= 128){ //Would be considered signed, pad it with an extra zero $sString = "\x00" . $sString; } //0x02 = Integer ASN.1 tag $sequence = "\x02" . chr(strlen($rString)) . $rString . "\x02" . chr(strlen($sString)) . $sString; //0x30 = Sequence ASN.1 tag $derSignature = "\x30" . chr(strlen($sequence)) . $sequence; $v = openssl_verify("$headB64.$payloadB64", $derSignature, "-----BEGIN PUBLIC KEY-----\n" . wordwrap($currentPublicKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----\n", OPENSSL_ALGO_SHA384); if($v !== 1){ throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.badSignature"); } if($currentPublicKey === self::MOJANG_ROOT_PUBLIC_KEY){ $this->authenticated = true; //we're signed into xbox live } $claims = json_decode(base64_decode(strtr($payloadB64, '-_', '+/'), true), true); $time = time(); if(isset($claims["nbf"]) and $claims["nbf"] > $time){ throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.tooEarly"); } if(isset($claims["exp"]) and $claims["exp"] < $time){ throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.tooLate"); } $currentPublicKey = $claims["identityPublicKey"] ?? null; //if there are further links, the next link should be signed with this } public function onCompletion(Server $server) : void{ /** @var Player $player */ $player = $this->fetchLocal(); if($player->isClosed()){ $server->getLogger()->error("Player " . $player->getName() . " was disconnected before their login could be verified"); }else{ $player->onVerifyCompleted($this->authenticated, $this->error); } } }