From 3e98f8be718578644bb15ee6a992a875f6468e8f Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Fri, 27 Dec 2013 13:14:40 +0100 Subject: Add some code for S/MIME signatures verification, update Crypt_GPG package --- plugins/enigma/README | 1 + plugins/enigma/enigma.php | 21 ++- plugins/enigma/lib/enigma_driver_gnupg.php | 54 +++---- plugins/enigma/lib/enigma_driver_phpssl.php | 238 ++++++++++++++++++++++++++++ plugins/enigma/lib/enigma_engine.php | 51 ++++-- plugins/enigma/lib/enigma_error.php | 4 +- plugins/enigma/localization/en_US.inc | 7 +- 7 files changed, 323 insertions(+), 53 deletions(-) create mode 100644 plugins/enigma/lib/enigma_driver_phpssl.php (limited to 'plugins') diff --git a/plugins/enigma/README b/plugins/enigma/README index 22d6e513a..c4e474be2 100644 --- a/plugins/enigma/README +++ b/plugins/enigma/README @@ -12,6 +12,7 @@ Enigma Plugin Status: - Handling of PGP keys files attached to incoming messages - PGP encrypted messages decryption (started) - PGP keys management UI (started) +- S/MIME signatures verification (started) * TODO (must have): diff --git a/plugins/enigma/enigma.php b/plugins/enigma/enigma.php index 25520a27d..870b923b6 100644 --- a/plugins/enigma/enigma.php +++ b/plugins/enigma/enigma.php @@ -179,10 +179,11 @@ class enigma extends rcube_plugin { // add labels $this->add_texts('localization/'); - +/* $p['list']['enigmasettings'] = array( 'id' => 'enigmasettings', 'section' => $this->gettext('enigmasettings'), ); +*/ $p['list']['enigmacerts'] = array( 'id' => 'enigmacerts', 'section' => $this->gettext('enigmacerts'), ); @@ -203,11 +204,13 @@ class enigma extends rcube_plugin */ function preferences_list($p) { +/* if ($p['section'] == 'enigmasettings') { // This makes that section is not removed from the list $p['blocks']['dummy']['options']['dummy'] = array(); } - else if ($p['section'] == 'enigmacerts') { + else */ + if ($p['section'] == 'enigmacerts') { // This makes that section is not removed from the list $p['blocks']['dummy']['options']['dummy'] = array(); } @@ -313,18 +316,24 @@ class enigma extends rcube_plugin $attrib['id'] = 'enigma-message'; if ($sig instanceof enigma_signature) { - if ($sig->valid) { + $sender = ($sig->name ? $sig->name . ' ' : '') . '<' . $sig->email . '>'; + + if ($sig->valid === enigma_error::E_UNVERIFIED) { + $attrib['class'] = 'enigmawarning'; + $msg = str_replace('$sender', $sender, $this->gettext('sigunverified')); + $msg = str_replace('$keyid', $sig->id, $msg); + $msg = rcube::Q($msg); + } + else if ($sig->valid) { $attrib['class'] = 'enigmanotice'; - $sender = ($sig->name ? $sig->name . ' ' : '') . '<' . $sig->email . '>'; $msg = rcube::Q(str_replace('$sender', $sender, $this->gettext('sigvalid'))); } else { $attrib['class'] = 'enigmawarning'; - $sender = ($sig->name ? $sig->name . ' ' : '') . '<' . $sig->email . '>'; $msg = rcube::Q(str_replace('$sender', $sender, $this->gettext('siginvalid'))); } } - else if ($sig->getCode() == enigma_error::E_KEYNOTFOUND) { + else if ($sig && $sig->getCode() == enigma_error::E_KEYNOTFOUND) { $attrib['class'] = 'enigmawarning'; $msg = rcube::Q(str_replace('$keyid', enigma_key::format_id($sig->getData('id')), $this->gettext('signokey'))); diff --git a/plugins/enigma/lib/enigma_driver_gnupg.php b/plugins/enigma/lib/enigma_driver_gnupg.php index 5aa32217e..c4280a089 100644 --- a/plugins/enigma/lib/enigma_driver_gnupg.php +++ b/plugins/enigma/lib/enigma_driver_gnupg.php @@ -76,7 +76,7 @@ class enigma_driver_gnupg extends enigma_driver // Create Crypt_GPG object try { - $this->gpg = new Crypt_GPG(array( + $this->gpg = new Crypt_GPG(array( 'homedir' => $this->homedir, // 'debug' => true, )); @@ -89,20 +89,20 @@ class enigma_driver_gnupg extends enigma_driver function encrypt($text, $keys) { /* - foreach ($keys as $key) { - $this->gpg->addEncryptKey($key); - } - $enc = $this->gpg->encrypt($text); - return $enc; + foreach ($keys as $key) { + $this->gpg->addEncryptKey($key); + } + $enc = $this->gpg->encrypt($text); + return $enc; */ } function decrypt($text, $key, $passwd) { -// $this->gpg->addDecryptKey($key, $passwd); +// $this->gpg->addDecryptKey($key, $passwd); try { - $dec = $this->gpg->decrypt($text); - return $dec; + $dec = $this->gpg->decrypt($text); + return $dec; } catch (Exception $e) { return $this->get_error_from_exception($e); @@ -112,17 +112,17 @@ class enigma_driver_gnupg extends enigma_driver function sign($text, $key, $passwd) { /* - $this->gpg->addSignKey($key, $passwd); - $signed = $this->gpg->sign($text, Crypt_GPG::SIGN_MODE_DETACHED); - return $signed; + $this->gpg->addSignKey($key, $passwd); + $signed = $this->gpg->sign($text, Crypt_GPG::SIGN_MODE_DETACHED); + return $signed; */ } function verify($text, $signature) { try { - $verified = $this->gpg->verify($text, $signature); - return $this->parse_signature($verified[0]); + $verified = $this->gpg->verify($text, $signature); + return $this->parse_signature($verified[0]); } catch (Exception $e) { return $this->get_error_from_exception($e); @@ -141,11 +141,11 @@ class enigma_driver_gnupg extends enigma_driver return $this->get_error_from_exception($e); } } - + public function list_keys($pattern='') { try { - $keys = $this->gpg->getKeys($pattern); + $keys = $this->gpg->getKeys($pattern); $result = array(); //print_r($keys); foreach ($keys as $idx => $key) { @@ -153,13 +153,13 @@ class enigma_driver_gnupg extends enigma_driver unset($keys[$idx]); } //print_r($result); - return $result; + return $result; } catch (Exception $e) { return $this->get_error_from_exception($e); } } - + public function get_key($keyid) { $list = $this->list_keys($keyid); @@ -167,7 +167,7 @@ class enigma_driver_gnupg extends enigma_driver if (is_array($list)) return array_shift($list); - // error + // error return $list; } @@ -178,14 +178,12 @@ class enigma_driver_gnupg extends enigma_driver public function del_key($keyid) { // $this->get_key($keyid); - - } - + public function del_privkey($keyid) { try { - $this->gpg->deletePrivateKey($keyid); + $this->gpg->deletePrivateKey($keyid); return true; } catch (Exception $e) { @@ -196,14 +194,14 @@ class enigma_driver_gnupg extends enigma_driver public function del_pubkey($keyid) { try { - $this->gpg->deletePublicKey($keyid); + $this->gpg->deletePublicKey($keyid); return true; } catch (Exception $e) { return $this->get_error_from_exception($e); } } - + /** * Converts Crypt_GPG exception into Enigma's error object * @@ -281,7 +279,7 @@ class enigma_driver_gnupg extends enigma_driver $ekey->users[$idx] = $id; } - + $ekey->name = trim($ekey->users[0]->name . ' <' . $ekey->users[0]->email . '>'); foreach ($key->getSubKeys() as $idx => $subkey) { @@ -297,9 +295,9 @@ class enigma_driver_gnupg extends enigma_driver $ekey->subkeys[$idx] = $skey; }; - + $ekey->id = $ekey->subkeys[0]->id; - + return $ekey; } } diff --git a/plugins/enigma/lib/enigma_driver_phpssl.php b/plugins/enigma/lib/enigma_driver_phpssl.php new file mode 100644 index 000000000..50af44762 --- /dev/null +++ b/plugins/enigma/lib/enigma_driver_phpssl.php @@ -0,0 +1,238 @@ + | + +-------------------------------------------------------------------------+ +*/ + +class enigma_driver_phpssl extends enigma_driver +{ + private $rc; + //private $gpg; + private $homedir; + private $user; + + function __construct($user) + { + $rcmail = rcmail::get_instance(); + $this->rc = $rcmail; + $this->user = $user; + } + + /** + * Driver initialization and environment checking. + * Should only return critical errors. + * + * @return mixed NULL on success, enigma_error on failure + */ + function init() + { + $homedir = $this->rc->config->get('enigma_smime_homedir', INSTALL_PATH . '/plugins/enigma/home'); + + if (!$homedir) + return new enigma_error(enigma_error::E_INTERNAL, + "Option 'enigma_smime_homedir' not specified"); + + // check if homedir exists (create it if not) and is readable + if (!file_exists($homedir)) + return new enigma_error(enigma_error::E_INTERNAL, + "Keys directory doesn't exists: $homedir"); + if (!is_writable($homedir)) + return new enigma_error(enigma_error::E_INTERNAL, + "Keys directory isn't writeable: $homedir"); + + $homedir = $homedir . '/' . $this->user; + + // check if user's homedir exists (create it if not) and is readable + if (!file_exists($homedir)) + mkdir($homedir, 0700); + + if (!file_exists($homedir)) + return new enigma_error(enigma_error::E_INTERNAL, + "Unable to create keys directory: $homedir"); + if (!is_writable($homedir)) + return new enigma_error(enigma_error::E_INTERNAL, + "Unable to write to keys directory: $homedir"); + + $this->homedir = $homedir; + + } + + function encrypt($text, $keys) + { + } + + function decrypt($text, $key, $passwd) + { + } + + function sign($text, $key, $passwd) + { + } + + function verify($struct, $message) + { + // use common temp dir + $temp_dir = $this->rc->config->get('temp_dir'); + $msg_file = tempnam($temp_dir, 'rcmMsg'); + $cert_file = tempnam($temp_dir, 'rcmCert'); + + $fh = fopen($msg_file, "w"); + if ($struct->mime_id) { + $message->get_part_content($struct->mime_id, $fh, true, 0, false); + } + else { + $this->rc->storage->get_raw_body($message->uid, $fh); + } + fclose($fh); + + // @TODO: use stored certificates + + // try with certificate verification + $sig = openssl_pkcs7_verify($msg_file, 0, $cert_file); + $validity = true; + + if ($sig !== true) { + // try without certificate verification + $sig = openssl_pkcs7_verify($msg_file, PKCS7_NOVERIFY, $cert_file); + $validity = enigma_error::E_UNVERIFIED; + } + + if ($sig === true) { + $sig = $this->parse_sig_cert($cert_file, $validity); + } + else { + $errorstr = $this->get_openssl_error(); + $sig = new enigma_error(enigma_error::E_INTERNAL, $errorstr); + } + + // remove temp files + @unlink($msg_file); + @unlink($cert_file); + + return $sig; + } + + public function import($content, $isfile=false) + { + } + + public function list_keys($pattern='') + { + } + + public function get_key($keyid) + { + } + + public function gen_key($data) + { + } + + public function del_key($keyid) + { + } + + public function del_privkey($keyid) + { + } + + public function del_pubkey($keyid) + { + } + + /** + * Converts Crypt_GPG_Key object into Enigma's key object + * + * @param Crypt_GPG_Key Key object + * + * @return enigma_key Key object + */ + private function parse_key($key) + { +/* + $ekey = new enigma_key(); + + foreach ($key->getUserIds() as $idx => $user) { + $id = new enigma_userid(); + $id->name = $user->getName(); + $id->comment = $user->getComment(); + $id->email = $user->getEmail(); + $id->valid = $user->isValid(); + $id->revoked = $user->isRevoked(); + + $ekey->users[$idx] = $id; + } + + $ekey->name = trim($ekey->users[0]->name . ' <' . $ekey->users[0]->email . '>'); + + foreach ($key->getSubKeys() as $idx => $subkey) { + $skey = new enigma_subkey(); + $skey->id = $subkey->getId(); + $skey->revoked = $subkey->isRevoked(); + $skey->created = $subkey->getCreationDate(); + $skey->expires = $subkey->getExpirationDate(); + $skey->fingerprint = $subkey->getFingerprint(); + $skey->has_private = $subkey->hasPrivate(); + $skey->can_sign = $subkey->canSign(); + $skey->can_encrypt = $subkey->canEncrypt(); + + $ekey->subkeys[$idx] = $skey; + }; + + $ekey->id = $ekey->subkeys[0]->id; + + return $ekey; +*/ + } + + private function get_openssl_error() + { + $tmp = array(); + while ($errorstr = openssl_error_string()) { + $tmp[] = $errorstr; + } + + return join("\n", array_values($tmp)); + } + + private function parse_sig_cert($file, $validity) + { + $cert = openssl_x509_parse(file_get_contents($file)); + + if (empty($cert) || empty($cert['subject'])) { + $errorstr = $this->get_openssl_error(); + return new enigma_error(enigm_error::E_INTERNAL, $errorstr); + } + + $data = new enigma_signature(); + + $data->id = $cert['hash']; //? + $data->valid = $validity; + $data->fingerprint = $cert['serialNumber']; + $data->created = $cert['validFrom_time_t']; + $data->expires = $cert['validTo_time_t']; + $data->name = $cert['subject']['CN']; +// $data->comment = ''; + $data->email = $cert['subject']['emailAddress']; + + return $data; + } + +} diff --git a/plugins/enigma/lib/enigma_engine.php b/plugins/enigma/lib/enigma_engine.php index 8a64c07ff..e4972c6a9 100644 --- a/plugins/enigma/lib/enigma_engine.php +++ b/plugins/enigma/lib/enigma_engine.php @@ -92,9 +92,6 @@ class enigma_engine if ($this->smime_driver) return; - // NOT IMPLEMENTED! - return; - $driver = 'enigma_driver_' . $this->rc->config->get('enigma_smime_driver', 'phpssl'); $username = $this->rc->user->get_username(); @@ -246,7 +243,7 @@ class enigma_engine fclose($fh); } - + /** * Handler for PGP/MIME signed message. * Verifies signature. @@ -255,14 +252,14 @@ class enigma_engine */ private function parse_pgp_signed(&$p) { - $this->load_pgp_driver(); - $struct = $p['structure']; - // Verify signature if ($this->rc->action == 'show' || $this->rc->action == 'preview') { + $this->load_pgp_driver(); + $struct = $p['structure']; + $msg_part = $struct->parts[0]; $sig_part = $struct->parts[1]; - + // Get bodies $this->set_part_body($msg_part, $p['object']->uid); $this->set_part_body($sig_part, $p['object']->uid); @@ -294,7 +291,31 @@ class enigma_engine */ private function parse_smime_signed(&$p) { - $this->load_smime_driver(); + // Verify signature + if ($this->rc->action == 'show' || $this->rc->action == 'preview') { + $this->load_smime_driver(); + + $struct = $p['structure']; + $msg_part = $struct->parts[0]; + + // Verify + $sig = $this->smime_driver->verify($struct, $p['object']); + + // Store signature data for display + $this->signatures[$struct->mime_id] = $sig; + + // Message can be multipart (assign signature to each subpart) + if (!empty($msg_part->parts)) { + foreach ($msg_part->parts as $part) + $this->signed_parts[$part->mime_id] = $struct->mime_id; + } + else { + $this->signed_parts[$msg_part->mime_id] = $struct->mime_id; + } + + // Remove signature file from attachments list + unset($struct->parts[1]); + } } /** @@ -306,22 +327,22 @@ class enigma_engine { $this->load_pgp_driver(); $part = $p['structure']; - + // Get body $this->set_part_body($part, $p['object']->uid); - // Decrypt + // Decrypt $result = $this->pgp_decrypt($part->body); - + // Store decryption status $this->decryptions[$part->mime_id] = $result; - + // Parse decrypted message if ($result === true) { // @TODO } } - + /** * Handler for PGP/MIME encrypted message. * @@ -359,7 +380,7 @@ class enigma_engine */ private function parse_smime_encrypted(&$p) { - $this->load_smime_driver(); +// $this->load_smime_driver(); } /** diff --git a/plugins/enigma/lib/enigma_error.php b/plugins/enigma/lib/enigma_error.php index 9f424dc2b..ab8d01557 100644 --- a/plugins/enigma/lib/enigma_error.php +++ b/plugins/enigma/lib/enigma_error.php @@ -34,7 +34,9 @@ class enigma_error const E_KEYNOTFOUND = 3; const E_DELKEY = 4; const E_BADPASS = 5; - + const E_EXPIRED = 6; + const E_UNVERIFIED = 7; + function __construct($code = null, $message = '', $data = array()) { $this->code = $code; diff --git a/plugins/enigma/localization/en_US.inc b/plugins/enigma/localization/en_US.inc index e0f03d9a0..f1ff8d1fb 100644 --- a/plugins/enigma/localization/en_US.inc +++ b/plugins/enigma/localization/en_US.inc @@ -1,9 +1,9 @@