/**
* 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();
}