View source
<?php
namespace Drupal\Signify;
class Verifier {
const COMMENTHDR = 'untrusted comment: ';
const COMMENTHDRLEN = 19;
const COMMENTMAXLEN = 1024;
protected $publicKeyRaw;
protected $publicKey;
protected $now;
function __construct($public_key_raw) {
$this->publicKeyRaw = $public_key_raw;
}
public function getPublicKeyRaw() {
return $this->publicKeyRaw;
}
public function getPublicKey() {
if (!$this->publicKey) {
$this->publicKey = $this
->parseB64String($this->publicKeyRaw, SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES);
}
return $this->publicKey;
}
public function getNow(\DateTime $now = NULL) {
$date_format = 'Y-m-d';
if (empty($now)) {
$now = gmdate($date_format);
}
else {
$now = $now
->format($date_format);
}
$now_dt = \DateTime::createFromFormat($date_format, $now, new \DateTimeZone('UTC'));
if (!$now_dt instanceof \DateTime) {
throw new VerifierException('Unexpected date format of current date.');
}
return $now_dt;
}
public function parseB64String($b64, $length) {
$parts = explode("\n", $b64);
if (count($parts) !== 3) {
throw new VerifierException("Invalid format; must contain two newlines, one after comment and one after base64");
}
$comment = $parts[0];
if (substr($comment, 0, self::COMMENTHDRLEN) !== self::COMMENTHDR) {
throw new VerifierException(sprintf("Invalid format; comment must start with '%s'", self::COMMENTHDR));
}
if (strlen($comment) > self::COMMENTHDRLEN + self::COMMENTMAXLEN) {
throw new VerifierException(sprintf("Invalid format; comment longer than %d bytes", self::COMMENTMAXLEN));
}
return new VerifierB64Data($parts[1], $length);
}
public function verifyMessage($signed_message) {
$pubkey = $this
->getPublicKey();
$embedded_message_index = 0;
for ($i = 1; $i <= 2 && $embedded_message_index !== false; $i++) {
$embedded_message_index = strpos($signed_message, "\n", $embedded_message_index + 1);
}
$signature = substr($signed_message, 0, $embedded_message_index + 1);
$message = substr($signed_message, $embedded_message_index + 1);
if ($message === false) {
$message = '';
}
$sig = $this
->parseB64String($signature, SODIUM_CRYPTO_SIGN_BYTES);
if ($pubkey->keyNum !== $sig->keyNum) {
throw new VerifierException('verification failed: checked against wrong key');
}
$valid = sodium_crypto_sign_verify_detached($sig->data, $message, $pubkey->data);
if (!$valid) {
throw new VerifierException('Signature did not match');
}
return $message;
}
public function verifyChecksumList($signed_checksum_list, $working_directory) {
$checksum_list_raw = $this
->verifyMessage($signed_checksum_list);
return $this
->verifyTrustedChecksumList($checksum_list_raw, $working_directory);
}
protected function verifyTrustedChecksumList($checksum_list_raw, $working_directory) {
$checksum_list = new ChecksumList($checksum_list_raw, true);
$failed_checksum_list = new FailedCheckumFilter($checksum_list, $working_directory);
foreach ($failed_checksum_list as $file_checksum) {
$actual_hash = @hash_file(strtolower($file_checksum->algorithm), $working_directory . DIRECTORY_SEPARATOR . $file_checksum->filename);
if ($actual_hash === false) {
throw new VerifierException("File \"{$file_checksum->filename}\" in the checksum list could not be read.");
}
if (empty($actual_hash) || strlen($actual_hash) < 64) {
throw new VerifierException("Failure computing hash for file \"{$file_checksum->filename}\" in the checksum list.");
}
if ($actual_hash !== $file_checksum->hex_hash) {
throw new VerifierException("File \"{$file_checksum->filename}\" does not pass checksum verification.");
}
}
return $checksum_list
->count();
}
public function verifyChecksumFile($checksum_file) {
$absolute_path = realpath($checksum_file);
if (empty($absolute_path)) {
throw new VerifierException("The real path of checksum list file at \"{$checksum_file}\" could not be determined.");
}
$working_directory = dirname($absolute_path);
if (is_dir($absolute_path)) {
throw new VerifierException("The checksum list file at \"{$checksum_file}\" is a directory, not a file.");
}
$signed_checksum_list = @file_get_contents($absolute_path);
if (empty($signed_checksum_list)) {
throw new VerifierException("The checksum list file at \"{$checksum_file}\" could not be read.");
}
return $this
->verifyChecksumList($signed_checksum_list, $working_directory);
}
public function verifyCsigMessage($chained_signed_message, \DateTime $now = NULL) {
$csig_lines = explode("\n", $chained_signed_message, 6);
$root_signed_intermediate_key_and_validity = implode("\n", array_slice($csig_lines, 0, 5)) . "\n";
$this
->verifyMessage($root_signed_intermediate_key_and_validity);
$valid_through_dt = \DateTime::createFromFormat('Y-m-d', $csig_lines[2], new \DateTimeZone('UTC'));
if (!$valid_through_dt instanceof \DateTime) {
throw new VerifierException('Unexpected valid-through date format.');
}
$now_dt = $this
->getNow($now);
$diff = $now_dt
->diff($valid_through_dt);
if ($diff->invert) {
throw new VerifierException(sprintf('The intermediate key expired %d day(s) ago.', $diff->days));
}
$intermediate_pubkey = implode("\n", array_slice($csig_lines, 3, 2)) . "\n";
$chained_verifier = new self($intermediate_pubkey);
$signed_message = implode("\n", array_slice($csig_lines, 5));
return $chained_verifier
->verifyMessage($signed_message);
}
public function verifyCsigChecksumList($csig_signed_checksum_list, $working_directory, \DateTime $now = NULL) {
$checksum_list_raw = $this
->verifyCsigMessage($csig_signed_checksum_list, $now);
return $this
->verifyTrustedChecksumList($checksum_list_raw, $working_directory);
}
public function verifyCsigChecksumFile($csig_checksum_file, \DateTime $now = NULL) {
$absolute_path = realpath($csig_checksum_file);
if (empty($absolute_path)) {
throw new VerifierException("The real path of checksum list file at \"{$csig_checksum_file}\" could not be determined.");
}
$working_directory = dirname($absolute_path);
if (is_dir($absolute_path)) {
throw new VerifierException("The checksum list file at \"{$csig_checksum_file}\" is a directory, not a file.");
}
$signed_checksum_list = file_get_contents($absolute_path);
if (empty($signed_checksum_list)) {
throw new VerifierException("The checksum list file at \"{$csig_checksum_file}\" could not be read.");
}
return $this
->verifyCsigChecksumList($signed_checksum_list, $working_directory, $now);
}
}