Skip to content

Commit

Permalink
Merge pull request #28 from paragonie/chronicle-check-rarely
Browse files Browse the repository at this point in the history
Only query Chronicle during RemoteFetch.
  • Loading branch information
paragonie-scott authored Sep 27, 2019
2 parents 67c01d4 + c0813ee commit cc39b91
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 13 deletions.
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
],
"require": {
"php": "^5.5|^7",
"ext-curl": "*",
"ext-json": "*",
"guzzlehttp/guzzle": "^6",
"paragonie/constant_time_encoding": "^1|^2",
"paragonie/sodium_compat": "^1.11"
Expand Down
92 changes: 79 additions & 13 deletions src/Fetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class Fetch
*/
protected $chroniclePublicKey = '';

/**
* List of bundles that have just been downloaded (e.g. RemoteFetch)
* @var array<int, string> $unverified
*/
protected $unverified = [];

/**
* Fetch constructor.
*
Expand Down Expand Up @@ -70,10 +76,13 @@ public function getLatestBundle($checkEd25519Signature = null, $checkChronicle =
if (\is_null($checkEd25519Signature)) {
$checkEd25519Signature = (bool) (static::CHECK_SIGNATURE_BY_DEFAULT && $sodiumCompatIsntSlow);
}
if (\is_null($checkChronicle)) {
$conditionalChronicle = \is_null($checkChronicle);
if ($conditionalChronicle) {
$checkChronicle = (bool) (static::CHECK_CHRONICLE_BY_DEFAULT && $sodiumCompatIsntSlow);
}

/** @var int $bundleIndex */
$bundleIndex = 0;
/** @var Bundle $bundle */
foreach ($this->listBundles('', $this->trustChannel) as $bundle) {
if ($bundle->hasCustom()) {
Expand All @@ -88,14 +97,33 @@ public function getLatestBundle($checkEd25519Signature = null, $checkChronicle =
$valid = true;
if ($checkEd25519Signature) {
$valid = $valid && $validator->checkEd25519Signature($bundle);
if (!$valid) {
$this->markBundleAsBad($bundleIndex, 'Ed25519 signature mismatch');
}
}
if ($checkChronicle) {
if ($conditionalChronicle && $checkChronicle) {
// Conditional Chronicle check (only on first brush):
$index = array_search($bundle->getFilePath(), $this->unverified, true);
if ($index !== false) {
$validChronicle = $validator->checkChronicleHash($bundle);
$valid = $valid && $validChronicle;
if ($validChronicle) {
unset($this->unverified[$index]);
} else {
$this->markBundleAsBad($bundleIndex, 'Chronicle');
}
}
} elseif ($checkChronicle) {
// Always check Chronicle:
$valid = $valid && $validator->checkChronicleHash($bundle);
}
if ($valid) {
return $bundle;
}
} else {
$this->markBundleAsBad($bundleIndex, 'SHA256 mismatch');
}
++$bundleIndex;
}
throw new BundleException('No valid bundles were found in the data directory.');
}
Expand Down Expand Up @@ -133,18 +161,31 @@ public function setChronicle($url, $publicKey)
}

/**
* List bundles
*
* @param string $customValidator Fully-qualified class name for Validator
* @param string $trustChannel
* @return array<int, Bundle>
*
* @throws CertaintyException
* @param int $index
* @param string $reason
* @return void
* @throws EncodingException
* @throws FilesystemException
*/
protected function listBundles(
$customValidator = '',
$trustChannel = Certainty::TRUST_DEFAULT
) {
protected function markBundleAsBad($index = 0, $reason = '')
{
/** @var array<int, array<string, string>> $data */
$data = $this->loadCaCertsFile();
$now = (new \DateTime())->format(\DateTime::ATOM);
$data[$index]['bad-bundle'] = 'Marked bad on ' . $now . ' for reason: ' . $reason;
\file_put_contents(
$this->dataDirectory . '/ca-certs.json',
json_encode($data, JSON_PRETTY_PRINT)
);
}

/**
* @return array
* @throws EncodingException
* @throws FilesystemException
*/
protected function loadCaCertsFile()
{
if (!\file_exists($this->dataDirectory . '/ca-certs.json')) {
throw new FilesystemException('ca-certs.json not found in data directory.');
}
Expand All @@ -160,13 +201,38 @@ protected function listBundles(
if (!\is_array($data)) {
throw new EncodingException('ca-certs.json is not a valid JSON file.');
}
return (array) $data;
}

/**
* List bundles
*
* @param string $customValidator Fully-qualified class name for Validator
* @param string $trustChannel
* @return array<int, Bundle>
*
* @throws CertaintyException
*/
protected function listBundles(
$customValidator = '',
$trustChannel = Certainty::TRUST_DEFAULT
) {
$data = $this->loadCaCertsFile();
$bundles = [];
/** @var array<string, string> $row */
foreach ($data as $row) {
if (!isset($row['date'], $row['file'], $row['sha256'], $row['signature'], $row['trust-channel'])) {
// The necessary keys are not defined.
continue;
}
if (!file_exists($this->dataDirectory . '/' . $row['file'])) {
// Skip nonexistent files
continue;
}
if (!empty($row['bad-bundle'])) {
// Bundle marked as "bad"
continue;
}
if ($row['trust-channel'] !== $trustChannel) {
// Only include these.
continue;
Expand Down
1 change: 1 addition & 0 deletions src/RemoteFetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ protected function remoteFetchBundles()
/** @var string $body */
$body = (string) $request->getBody();
\file_put_contents($this->dataDirectory . '/' . $filename, $body);
$this->unverified []= $this->dataDirectory . '/' . $item['file'];
}
}

Expand Down
8 changes: 8 additions & 0 deletions test/RemoteFetchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,13 @@ public function testRemoteFetch()
);
$fetch->setCacheTimeout(new \DateInterval('PT01M'));
$this->assertTrue($fetch->cacheExpired());


$latest = $fetch->getLatestBundle();
file_put_contents($latest->getFilePath(), ' corrupt', FILE_APPEND);
(new RemoteFetch($this->dir))->getLatestBundle();

$cacerts = json_decode(file_get_contents($this->dir . '/ca-certs.json'), true);
$this->assertTrue(!empty($cacerts[0]['bad-bundle']));
}
}

0 comments on commit cc39b91

Please sign in to comment.