Skip to content

Commit

Permalink
Begin Chronicle integration.
Browse files Browse the repository at this point in the history
  • Loading branch information
paragonie-security committed Oct 31, 2017
1 parent 509e3cb commit 45311ab
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,4 +1,5 @@
/.idea
/local/dev-testing
/local/keys.json
/composer.lock
/vendor
4 changes: 4 additions & 0 deletions data/ca-certs.json
@@ -1,23 +1,27 @@
[
{
"chronicle": "pTmauXUmQrr2BN8uJX3mCKk0GSokHl61qHUrXsUFziE=",
"date": "2017-09-20",
"file": "cacert-2017-09-20.pem",
"sha256": "435ac8e816f5c10eaaf228d618445811c16a5e842e461cb087642b6265a36856",
"signature": "9007f7f0411d6d1f1f5136b247375e614a24216e4fc6c9d6d12642f986f3d45cea3daa2a19705579845a37488ce679f78a1b890d24da6157a2e9894d351fa70a"
},
{
"chronicle": "tUgevWspRLIznoIx0G6XRMucU4XJSBV3qYZEPWovZV8=",
"date": "2017-06-07",
"file": "cacert-2017-06-07.pem",
"sha256": "e78c8ab7b4432bd466e64bb942d988f6c0ac91cd785017e465bdc96d42fe9dd0",
"signature": "ed1fc6af6827cac04da6caf40deffeadc2a19feba5281d7cf92d1563ad9af49b8d25bf459e5d5acec0fe723394f88f240d4b716e52f3835f9ab3caa3cc85380e"
},
{
"chronicle": "vkGXMsFKfLlQBh3uYUQbLFdXKgQe5huy-pZZ-9cIDJ4=",
"date": "2017-01-18",
"file": "cacert-2017-01-18.pem",
"sha256": "e62a07e61e5870effa81b430e1900778943c228bd7da1259dd6a955ee2262b47",
"signature": "0f217f29c9711cd74ed60f0f6da886c166969945546a6e75e6fa8cf5ea87387f5fce1e1ced71af46095d2dd411a3676ec1aa40927cc0d47a91adaeef965b240b"
},
{
"chronicle": "5dmkHGPHwnIOawjmnrbXBIXap92GqF2aDraASC12AVM=",
"date": "2016-11-02",
"file": "cacert-2016-11-02.pem",
"sha256": "cc7c9e2d259e20b72634371b146faec98df150d18dd9da9ad6ef0b2deac2a9d3",
Expand Down
18 changes: 17 additions & 1 deletion src/Bundle.php
Expand Up @@ -12,6 +12,9 @@
*/
class Bundle
{
/** @var string $chronicleHash */
protected $chronicleHash = '';

/** @var Validator $customValidator */
protected $customValidator;

Expand All @@ -31,17 +34,20 @@ class Bundle
* @param string $sha256sum Hex-encoded string
* @param string $signature Hex-encoded string
* @param string $customValidator Fully-Qualified Class Name
* @param string $chronicleHash Chronicle Hash
* @throws \TypeError
*/
public function __construct(
$filePath = '',
$sha256sum = '',
$signature = '',
$customValidator = ''
$customValidator = '',
$chronicleHash = ''
) {
$this->filePath = $filePath;
$this->sha256sum = $sha256sum;
$this->signature = $signature;
$this->chronicleHash = $chronicleHash;
$newClass = new Validator();
if (!empty($customValidator)) {
if (\class_exists($customValidator)) {
Expand Down Expand Up @@ -112,6 +118,16 @@ public function getSignature($raw = false)
return $this->signature;
}

/**
* Get the Chronicle hash (always base64url-encoded)
*
* @return string
*/
public function getChronicleHash()
{
return $this->chronicleHash;
}

/**
* Get the custom validator (assuming one is defined).
*
Expand Down
30 changes: 30 additions & 0 deletions src/Certainty.php
@@ -0,0 +1,30 @@
<?php
namespace ParagonIE\Certainty;

use GuzzleHttp\Client;

/**
* Class Certainty
* @package ParagonIE\Certainty
*/
class Certainty
{
const REPOSITORY = 'paragonie/certainty';
const ED25519_HEADER = 'Body-Signature-Ed25519';

/**
* @param Fetch|null $fetch
* @return Client
*/
public static function getGuzzleClient(Fetch $fetch = null)
{
if (\is_null($fetch)) {
$fetch = new Fetch();
}
return new Client(
[
'verify' => $fetch->getLatestBundle()->getFilePath()
]
);
}
}
35 changes: 23 additions & 12 deletions src/Fetch.php
Expand Up @@ -8,6 +8,7 @@
class Fetch
{
const CHECK_SIGNATURE_BY_DEFAULT = false;
const CHECK_CHRONICLE_BY_DEFAULT = false;

/** @var string $dataDirectory */
protected $dataDirectory = '';
Expand All @@ -33,29 +34,38 @@ public function __construct($dataDir = '')
* is expected. Optionally checks the Ed25519 signature.
*
* @param bool|null $checkEd25519Signature
* @param bool|null $checkChronicle
* @return Bundle
* @throws \Exception
*/
public function getLatestBundle($checkEd25519Signature = null)
public function getLatestBundle($checkEd25519Signature = null, $checkChronicle = null)
{
if (\is_null($checkEd25519Signature)) {
$checkEd25519Signature = (bool) static::CHECK_SIGNATURE_BY_DEFAULT;
}
if (\is_null($checkChronicle)) {
$checkChronicle = (bool) static::CHECK_CHRONICLE_BY_DEFAULT;
}

/** @var Bundle $bundle */
foreach ($this->listBundles() as $bundle) {
if ($bundle->hasCustom()) {
$validator = $bundle->getValidator();
if ($validator::checkSha256Sum($bundle)) {
if (!$checkEd25519Signature) {
return $bundle;
} elseif ($validator::checkEd25519Signature($bundle)) {
return $bundle;
}
} else {
$validator = new Validator();
}

// If the SHA256 doesn't match, fail fast.
if ($validator::checkSha256Sum($bundle)) {
/** @var bool $valid */
$valid = true;
if ($checkEd25519Signature) {
$valid = $valid && $validator::checkEd25519Signature($bundle);
}
} elseif (Validator::checkSha256Sum($bundle)) {
if (!$checkEd25519Signature) {
return $bundle;
} elseif (Validator::checkEd25519Signature($bundle)) {
if ($checkChronicle) {
$valid = $valid && $validator::checkChronicleHash($bundle);
}
if ($valid) {
return $bundle;
}
}
Expand Down Expand Up @@ -113,7 +123,8 @@ protected function listBundles($customValidator = '')
$this->dataDirectory . '/' . $row['file'],
$row['sha256'],
$row['signature'],
!empty($row['custom']) ? $row['custom'] : $customValidator
!empty($row['custom']) ? $row['custom'] : $customValidator,
isset($row['chronicle']) ? $row['chronicle'] : ''
);
}
\krsort($bundles);
Expand Down
121 changes: 119 additions & 2 deletions src/LocalCACertBuilder.php
@@ -1,6 +1,7 @@
<?php
namespace ParagonIE\Certainty;

use ParagonIE\ConstantTime\Base64UrlSafe;
use ParagonIE\ConstantTime\Hex;

/**
Expand All @@ -9,6 +10,21 @@
*/
class LocalCACertBuilder extends Bundle
{
/**
* @var string
*/
protected $chroniclePublicKey = '';

/**
* @var string
*/
protected $chronicleRepoName = 'paragonie/certainty';

/**
* @var string
*/
protected $chronicleUrl = '';

/**
* @var string
*/
Expand Down Expand Up @@ -89,6 +105,74 @@ public function appendCACertFile($path = '')
return $this;
}

/**
* Publish the most recent CACert information to the local Chronicle.
*
* @param string $sha256sum
* @param string $signature
* @return string
* @throws \Exception
*/
protected function commitToChronicle($sha256sum, $signature)
{
if (empty($this->chronicleUrl) || empty($this->chroniclePublicKey)) {
return '';
}

/** @var string $body */
$body = \json_encode(
[
'repository' => $this->chronicleRepoName,
'sha256' => $sha256sum,
'signature' => $signature,
'time' => (new \DateTime())->format(\DateTime::ATOM)
],
JSON_PRETTY_PRINT
);
if (!\is_string($body)) {
throw new \Exception('Could not build a valid JSON message.');
}
$signature = \ParagonIE_Sodium_Compat::crypto_sign_detached($body, $this->secretKey);

$http = Certainty::getGuzzleClient();
$response = $http->post(
$this->chronicleUrl . '/publish',
[
'headers' => [
Certainty::ED25519_HEADER => Base64UrlSafe::encode($signature)
],
'body' => $body,
]
);

$responseBody = (string) $response->getBody();
$validSig = false;
foreach ($response->getHeader(Certainty::ED25519_HEADER) as $sigLine) {
$sig = Base64UrlSafe::decode($sigLine);
$validSig = $validSig || \ParagonIE_Sodium_Compat::crypto_sign_verify_detached(
$sig,
$responseBody,
$this->chroniclePublicKey
);
}
if (!$validSig) {
throw new \Exception('Invalid response from Chronicle');
}

/** @var array $json */
$json = \json_decode($responseBody, true);
if (!\is_array($json)) {
return '';
}
if (!isset($json['results'])) {
return '';
}
if (!isset($json['results']['summaryhash'])) {
return '';
}
return (string) $json['results']['summaryhash'];
}

/**
* Get the public key.
*
Expand Down Expand Up @@ -145,13 +229,20 @@ public function save()
$pieces = \explode('/', \trim($this->outputPem, '/'));

// Put at the front of the array
\array_unshift($json, [
$entry = [
'custom' => \get_class($this->customValidator),
'date' => \date('Y-m-d'),
'file' => \array_pop($pieces),
'sha256' => $sha256sum,
'signature' => Hex::encode($signature)
]);
];

$chronicleHash = $this->commitToChronicle($sha256sum, $signature);
if (!empty($chronicleHash)) {
$entry['chronicle'] = $chronicleHash;
}

\array_unshift($json, $entry);
$jsonSave = \json_encode($json, JSON_PRETTY_PRINT);
if (!\is_string($jsonSave)) {
throw new \Exception(\json_last_error_msg());
Expand All @@ -163,6 +254,32 @@ public function save()
return \is_int($return);
}

/**
* Configure the local Chronicle.
*
* @param string $url
* @param string $publicKey
* @param string $repository
* @return $this
* @throws \Exception
*/
public function setChronicle($url = '', $publicKey = '', $repository = 'paragonie/certainty')
{
if (\ParagonIE_Sodium_Core_Util::strlen($publicKey) === 64) {
/** @var string $publicKey */
$publicKey = Hex::decode($publicKey);
if (!\is_string($publicKey)) {
throw new \Exception('Signing secret keys must be SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES bytes long.');
}
} elseif (\ParagonIE_Sodium_Core_Util::strlen($publicKey) !== 32) {
throw new \Exception('Signing secret keys must be SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES bytes long.');
}
$this->chroniclePublicKey = $publicKey;
$this->chronicleUrl = $url;
$this->chronicleRepoName = $repository;
return $this;
}

/**
* Specify the fully qualified class name for your custom
* Validator class.
Expand Down
5 changes: 2 additions & 3 deletions src/RemoteFetch.php
Expand Up @@ -13,6 +13,7 @@
class RemoteFetch extends Fetch
{
const CHECK_SIGNATURE_BY_DEFAULT = true;
const CHECK_CHRONICLE_BY_DEFAULT = true;
const DEFAULT_URL = 'https://raw.githubusercontent.com/paragonie/certainty/master/data/';

/** @var \DateInterval */
Expand Down Expand Up @@ -44,9 +45,7 @@ public function __construct(

if (\is_null($http)) {
if (\file_exists($this->dataDirectory . '/ca-certs.json')) {
$http = new Client([
'verify' => (new Fetch($this->dataDirectory))->getLatestBundle()->getFilePath()
]);
$http = Certainty::getGuzzleClient(new Fetch($this->dataDirectory));
} else {
$http = new Client();
}
Expand Down

0 comments on commit 45311ab

Please sign in to comment.