mirror of https://github.com/MISP/MISP
new: [GPG] Validate fetched GPG key
parent
3d872c8b4b
commit
380fba5405
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>';
|
||||
|
|
Loading…
Reference in New Issue