/** * Does this peer notary see the same Merkle root? * * @param Peer $peer * @param string $expectedRoot * @return bool * @throws CouldNotUpdate * @throws PeerSignatureFailed */ protected function checkWithPeer(Peer $peer, string $expectedRoot) : bool { foreach ($peer->getAllURLs('/verify') as $url) { // Challenge nonce: $challenge = Base64UrlSafe::encode(\random_bytes(33)); // Peer's response: $response = $this->hail->postJSON($url, ['challenge' => $challenge]); if ($response['status'] !== 'OK') { $this->log('Upstream error.', LogLevel::EMERGENCY, ['response' => $response]); return false; } // Decode then verify signature $message = Base64UrlSafe::decode($response['response']); $signature = Base64UrlSafe::decode($response['signature']); $isValid = AsymmetricCrypto::verify($message, $peer->getPublicKey(), $signature, true); if (!$isValid) { $this->log('Invalid digital signature (i.e. it was signed with an incorrect key).', LogLevel::EMERGENCY); throw new PeerSignatureFailed('Invalid digital signature (i.e. it was signed with an incorrect key).'); } // Make sure our challenge was signed. $decoded = \json_decode($message, true); if (!\hash_equals($challenge, $decoded['challenge'])) { $this->log('Challenge-response authentication failed.', LogLevel::EMERGENCY); throw new CouldNotUpdate(\__('Challenge-response authentication failed.')); } // Make sure this was a recent signature (it *should* be). // The earliest timestamp we will accept from a peer: $min = (new \DateTime('now'))->sub(new \DateInterval('P01D')); // The timestamp the server provided: $time = new \DateTime($decoded['timestamp']); if ($time < $min) { throw new CouldNotUpdate(\__('Timestamp %s is far too old.', 'default', $decoded['timestamp'])); } // Return TRUE if it matches the expected root. // Return FALSE if it matches. return \hash_equals($expectedRoot, $decoded['root']); } // When all else fails, throw a TransferException throw new TransferException(); }