new: [GPG] Validate fetched GPG key

pull/6300/head
Jakub Onderka 2020-09-08 23:03:48 +02:00
parent 3d872c8b4b
commit 380fba5405
5 changed files with 261 additions and 37 deletions

View File

@ -61,4 +61,91 @@ class CryptGpgExtended extends Crypt_GPG
return $keyData;
}
/**
* Return key info without importing it.
*
* @param string $key
* @return Crypt_GPG_Key[]
* @throws Crypt_GPG_Exception
* @throws Crypt_GPG_InvalidOperationException
*/
public function keyInfo($key)
{
$input = $this->_prepareInput($key, false, false);
$output = '';
$this->engine->reset();
$this->engine->setInput($input);
$this->engine->setOutput($output);
$this->engine->setOperation('--import', ['--import-options', 'show-only', '--with-colons']);
$this->engine->run();
$keys = array();
$key = null; // current key
$subKey = null; // current sub-key
foreach (explode(PHP_EOL, $output) as $line) {
$lineExp = explode(':', $line);
if ($lineExp[0] === 'pub') {
// new primary key means last key should be added to the array
if ($key !== null) {
$keys[] = $key;
}
$key = new Crypt_GPG_Key();
$subKey = Crypt_GPG_SubKey::parse($line);
$key->addSubKey($subKey);
} elseif ($lineExp[0] === 'sub') {
$subKey = Crypt_GPG_SubKey::parse($line);
$key->addSubKey($subKey);
} elseif ($lineExp[0] === 'fpr') {
$fingerprint = $lineExp[9];
// set current sub-key fingerprint
$subKey->setFingerprint($fingerprint);
} elseif ($lineExp[0] === 'uid') {
$string = stripcslashes($lineExp[9]); // as per documentation
$userId = new Crypt_GPG_UserId($string);
if ($lineExp[1] === 'r') {
$userId->setRevoked(true);
}
$key->addUserId($userId);
}
}
// add last key
if ($key !== null) {
$keys[] = $key;
}
return $keys;
}
/**
* @param string $key
* @return string
* @throws Crypt_GPG_Exception
* @throws Crypt_GPG_InvalidOperationException
*/
public function enarmor($key)
{
$input = $this->_prepareInput($key, false, false);
$armored = '';
$this->engine->reset();
$this->engine->setInput($input);
$this->engine->setOutput($armored);
$this->engine->setOperation('--enarmor');
$this->engine->run();
return $armored;
}
}

View File

@ -5,7 +5,7 @@ class GpgTool
* @return CryptGpgExtended
* @throws Exception
*/
public function initializeGpg()
public static function initializeGpg()
{
if (!class_exists('Crypt_GPG')) {
// 'Crypt_GPG' class cannot be autoloaded, try to require from include_path.
@ -31,6 +31,14 @@ class GpgTool
return new CryptGpgExtended($options);
}
/** @var CryptGpgExtended */
private $gpg;
public function __construct($gpg)
{
$this->gpg = $gpg;
}
/**
* @param string $search
* @return array
@ -65,9 +73,41 @@ class GpgTool
$key = $response->body;
if ($this->gpg) {
$fetchedFingerprint = $this->validateGpgKey($key);
if (strtolower($fingerprint) !== strtolower($fetchedFingerprint)) {
throw new Exception("Requested fingerprint do not match with fetched key fingerprint ($fingerprint != $fetchedFingerprint)");
}
}
return $key;
}
/**
* Validates PGP key
* @param string $keyData
* @return string Primary key fingerprint
* @throws Exception
*/
public function validateGpgKey($keyData)
{
if (!$this->gpg instanceof CryptGpgExtended) {
throw new InvalidArgumentException("Valid CryptGpgExtended instance required.");
}
$fetchedKeyInfo = $this->gpg->keyInfo($keyData);
if (empty($fetchedKeyInfo)) {
throw new Exception("No key found");
}
if (count($fetchedKeyInfo) !== 1) {
throw new Exception("Multiple keys found");
}
$primaryKey = $fetchedKeyInfo[0]->getPrimaryKey();
if (empty($primaryKey)) {
throw new Exception("No primary key found");
}
return $primaryKey->getFingerprint();
}
/**
* @param string $body
* @return array
@ -107,6 +147,91 @@ class GpgTool
return $final;
}
/**
* @see https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-10
* @param string $email
* @return string
* @throws Exception
*/
public function wkd($email)
{
if (!$this->gpg instanceof CryptGpgExtended) {
throw new InvalidArgumentException("Valid CryptGpgExtended instance required.");
}
$parts = explode('@', $email);
if (count($parts) !== 2) {
throw new InvalidArgumentException("Invalid e-mail address provided.");
}
list($localPart, $domain) = $parts;
$localPart = strtolower($localPart);
$localPartHash = $this->zbase32(sha1($localPart, true));
$advancedUrl = "https://openpgpkey.$domain/.well-known/openpgpkey/" . strtolower($domain) . "/hu/$localPartHash";
try {
$response = $this->keyServerLookup($advancedUrl);
return $this->processWkdResponse($response);
} catch (Exception $e) {
// pass, continue to direct method
}
$directUrl = "https://$domain/.well-known/openpgpkey/hu/$localPartHash";
$response = $this->keyServerLookup($directUrl);
return $this->processWkdResponse($response);
}
/**
* @param HttpSocketResponse $response
* @return string
* @throws Crypt_GPG_Exception
* @throws Crypt_GPG_InvalidOperationException
*/
private function processWkdResponse(HttpSocketResponse $response)
{
if ($response->code == 404) {
throw new NotFoundException("Key not found");
} else if (!$response->isOk()) {
throw new Exception("Fetching the WKD failed with HTTP error {$response->code}: {$response->reasonPhrase}");
}
return $this->gpg->enarmor($response->body());
}
/**
* Converts data to zbase32 string.
*
* @see http://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
* @param string $data
* @return string
*/
private function zbase32($data)
{
$chars = 'ybndrfg8ejkmcpqxot1uwisza345h769'; // lower-case
$res = '';
$remainder = 0;
$remainderSize = 0;
for ($i = 0; $i < strlen($data); $i++) {
$b = ord($data[$i]);
$remainder = ($remainder << 8) | $b;
$remainderSize += 8;
while ($remainderSize > 4) {
$remainderSize -= 5;
$c = $remainder & (31 << $remainderSize);
$c >>= $remainderSize;
$res .= $chars[$c];
}
}
if ($remainderSize > 0) {
// remainderSize < 5:
$remainder <<= (5 - $remainderSize);
$c = $remainder & 31;
$res .= $chars[$c];
}
return $res;
}
/**
* @param string $uri
* @return HttpSocketResponse

View File

@ -5202,9 +5202,8 @@ class Server extends AppModel
$gpgStatus = 0;
if (Configure::read('GnuPG.email') && Configure::read('GnuPG.homedir')) {
$continue = true;
$gpgTool = new GpgTool();
try {
$gpg = $gpgTool->initializeGpg();
$gpg = GpgTool::initializeGpg();
} catch (Exception $e) {
$this->logException("Error during initializing GPG.", $e, LOG_NOTICE);
$gpgStatus = 2;

View File

@ -222,7 +222,7 @@ class User extends AppModel
'Containable'
);
/** @var Crypt_GPG|null|false */
/** @var CryptGpgExtended|null|false */
private $gpg;
public function beforeValidate($options = array())
@ -302,9 +302,11 @@ class User extends AppModel
return true;
}
// Checks if the GnuPG key is a valid key, but also import it in the keychain.
// this will NOT fail on keys that can only be used for signing but not encryption!
// the method in verifyUsers will fail in that case.
/**
* Checks if the GnuPG key is a valid key.
* @param array $check
* @return bool
*/
public function validateGpgkey($check)
{
// LATER first remove the old gpgkey from the keychain
@ -319,12 +321,11 @@ class User extends AppModel
return true;
}
try {
$keyImportOutput = $gpg->importKey($check['gpgkey']);
if (!empty($keyImportOutput['fingerprint'])) {
return true;
}
$gpgTool = new GpgTool($gpg);
$gpgTool->validateGpgKey($check['gpgkey']);
return true;
} catch (Exception $e) {
$this->logException("Exception during importing GPG key", $e);
$this->logException("Exception during validating GPG key", $e, LOG_NOTICE);
return false;
}
}
@ -452,25 +453,40 @@ class User extends AppModel
)));
}
public function verifySingleGPG($user, $gpg = null)
/**
* 0 - true if key is valid
* 1 - User e-mail
* 2 - Error message
* 3 - Not used
* 4 - Key fingerprint
* 5 - Key fingerprint
* @param array $user
* @return array
*/
public function verifySingleGPG(array $user)
{
if ($gpg === null) {
$gpg = $this->initializeGpg();
if (!$gpg) {
$result[2] = 'GnuPG is not configured on this system.';
$result[0] = true;
return $result;
}
$result = [0 => false, 1 => $user['User']['email']];
$gpg = $this->initializeGpg();
if (!$gpg) {
$result[2] = 'GnuPG is not configured on this system.';
return $result;
}
$result = array();
try {
$currentTimestamp = time();
$temp = $gpg->importKey($user['User']['gpgkey']);
$key = $gpg->getKeys($temp['fingerprint']);
$result[5] = $temp['fingerprint'];
$subKeys = $key[0]->getSubKeys();
$sortedKeys = array('valid' => 0, 'expired' => 0, 'noEncrypt' => 0);
foreach ($subKeys as $subKey) {
$keys = $gpg->keyInfo($user['User']['gpgkey']);
if (count($keys) !== 1) {
$result[2] = 'Multiple or no key found';
return $result;
}
$key = $keys[0];
$result[4] = $key->getPrimaryKey()->getFingerprint();
$result[5] = $result[4];
$sortedKeys = ['valid' => 0, 'expired' => 0, 'noEncrypt' => 0];
foreach ($key->getSubKeys() as $subKey) {
$expiration = $subKey->getExpirationDate();
if ($expiration != 0 && $currentTimestamp > $expiration) {
$sortedKeys['expired']++;
@ -490,14 +506,12 @@ class User extends AppModel
if ($sortedKeys['noEncrypt']) {
$result[2] .= ' Found ' . $sortedKeys['noEncrypt'] . ' subkey(s) that are sign only.';
}
} else {
$result[0] = true;
}
} catch (Exception $e) {
$result[2] = $e->getMessage();
$result[0] = true;
}
$result[1] = $user['User']['email'];
$result[4] = $temp['fingerprint'];
return $result;
}
@ -521,7 +535,7 @@ class User extends AppModel
}
$results = [];
foreach ($users as $k => $user) {
$results[$user['User']['id']] = $this->verifySingleGPG($user, $gpg);
$results[$user['User']['id']] = $this->verifySingleGPG($user);
}
return $results;
}
@ -817,7 +831,7 @@ class User extends AppModel
*/
public function searchGpgKey($email)
{
$gpgTool = new GpgTool();
$gpgTool = new GpgTool(null);
return $gpgTool->searchGpgKey($email);
}
@ -828,7 +842,7 @@ class User extends AppModel
*/
public function fetchGpgKey($fingerprint)
{
$gpgTool = new GpgTool();
$gpgTool = new GpgTool($this->initializeGpg());
return $gpgTool->fetchGpgKey($fingerprint);
}
@ -1313,7 +1327,7 @@ class User extends AppModel
/**
* Initialize GPG. Returns `null` if initialization failed.
*
* @return null|Crypt_GPG
* @return null|CryptGpgExtended
*/
private function initializeGpg()
{
@ -1326,8 +1340,7 @@ class User extends AppModel
}
try {
$gpgTool = new GpgTool();
$this->gpg = $gpgTool->initializeGpg();
$this->gpg = GpgTool::initializeGpg();
return $this->gpg;
} catch (Exception $e) {
$this->logException("GPG couldn't be initialized, GPG encryption and signing will be not available.", $e, LOG_NOTICE);

View File

@ -3,7 +3,7 @@
<ul>
<?php foreach ($users as $k => $user) {
echo '<a href="'.$baseurl.'/admin/users/view/' . $k . '">' . $k . ' (' . h($user[1]) . ')</a>:';
if (isset($user[0])) {
if (!$user[0]) {
echo '-> <span style="color:red;"><span style="font-weight:bold">Invalid.</span> (' . h($user[2]) . ')</span>';
} else {
echo '-> <span style="color:green;">OK</span>';