def validate_key(key, sign=False, encrypt=False): """Assert that a key is valide and optionally that it can be used for signing or encrypting. Raise GPGProblem otherwise. :param key: the GPG key to check :type key: gpgme.Key :param sign: whether the key should be able to sign :type sign: bool :param encrypt: whether the key should be able to encrypt :type encrypt: bool """ if key.revoked: raise GPGProblem("The key \"" + key.uids[0].uid + "\" is revoked.", code=GPGCode.KEY_REVOKED) elif key.expired: raise GPGProblem("The key \"" + key.uids[0].uid + "\" is expired.", code=GPGCode.KEY_EXPIRED) elif key.invalid: raise GPGProblem("The key \"" + key.uids[0].uid + "\" is invalid.", code=GPGCode.KEY_INVALID) if encrypt and not key.can_encrypt: raise GPGProblem("The key \"" + key.uids[0].uid + "\" can not " + "encrypt.", code=GPGCode.KEY_CANNOT_ENCRYPT) if sign and not key.can_sign: raise GPGProblem("The key \"" + key.uids[0].uid + "\" can not sign.", code=GPGCode.KEY_CANNOT_SIGN)
def get_key(keyid, validate=False, encrypt=False, sign=False): """ Gets a key from the keyring by filtering for the specified keyid, but only if the given keyid is specific enough (if it matches multiple keys, an exception will be thrown). :param keyid: filter term for the keyring (usually a key ID) :rtype: gpgme.Key """ ctx = gpgme.Context() try: key = ctx.get_key(keyid) if validate: validate_key(key, encrypt=encrypt, sign=sign) except gpgme.GpgmeError as e: if e.code == gpgme.ERR_AMBIGUOUS_NAME: raise GPGProblem(("More than one key found matching this filter." + " Please be more specific (use a key ID like " + "4AC8EE1D)."), code=GPGCode.AMBIGUOUS_NAME) elif e.code == gpgme.ERR_INV_VALUE or e.code == gpgme.ERR_EOF: raise GPGProblem("Can not find key for \'" + keyid + "\'.", code=GPGCode.NOT_FOUND) else: raise e return key
def get_key(keyid, validate=False, encrypt=False, sign=False): """ Gets a key from the keyring by filtering for the specified keyid, but only if the given keyid is specific enough (if it matches multiple keys, an exception will be thrown). If validate is True also make sure that returned key is not invalid, revoked or expired. In addition if encrypt or sign is True also validate that key is valid for that action. For example only keys with private key can sign. :param keyid: filter term for the keyring (usually a key ID) :param validate: validate that returned keyid is valid :param encrypt: when validating confirm that returned key can encrypt :param sign: when validating confirm that returned key can sign :rtype: gpgme.Key """ ctx = gpgme.Context() try: key = ctx.get_key(keyid) if validate: validate_key(key, encrypt=encrypt, sign=sign) except gpgme.GpgmeError as e: if e.code == gpgme.ERR_AMBIGUOUS_NAME: # When we get here it means there were multiple keys returned by gpg # for given keyid. Unfortunately gpgme returns invalid and expired # keys together with valid keys. If only one key is valid for given # operation maybe we can still return it instead of raising # exception keys = list_keys(hint=keyid) valid_key = None for k in keys: try: validate_key(k, encrypt=encrypt, sign=sign) except GPGProblem: # if the key is invalid for given action skip it continue if valid_key: # we have already found one valid key and now we find # another? We really received an ambiguous keyid raise GPGProblem( ("More than one key found matching " + "this filter. Please be more " + "specific (use a key ID like " + "4AC8EE1D)."), code=GPGCode.AMBIGUOUS_NAME) valid_key = k if not valid_key: # there were multiple keys found but none of them are valid for # given action (we don't have private key, they are expired etc) raise GPGProblem("Can not find usable key for \'" + keyid + "\'.", code=GPGCode.NOT_FOUND) return valid_key elif e.code == gpgme.ERR_INV_VALUE or e.code == gpgme.ERR_EOF: raise GPGProblem("Can not find key for \'" + keyid + "\'.", code=GPGCode.NOT_FOUND) else: raise e return key
def _hash_algo_name(hash_algo): """ Re-implements GPGME's hash_algo_name as long as pygpgme doesn't wrap that function. :param hash_algo: GPGME hash_algo :rtype: str """ mapping = { gpgme.MD_MD5: "MD5", gpgme.MD_SHA1: "SHA1", gpgme.MD_RMD160: "RIPEMD160", gpgme.MD_MD2: "MD2", gpgme.MD_TIGER: "TIGER192", gpgme.MD_HAVAL: "HAVAL", gpgme.MD_SHA256: "SHA256", gpgme.MD_SHA384: "SHA384", gpgme.MD_SHA512: "SHA512", gpgme.MD_MD4: "MD4", gpgme.MD_CRC32: "CRC32", gpgme.MD_CRC32_RFC1510: "CRC32RFC1510", gpgme.MD_CRC24_RFC2440: "CRC24RFC2440", } if hash_algo in mapping: return mapping[hash_algo] else: raise GPGProblem(("Invalid hash_algo passed to hash_algo_name." " Please report this as a bug in alot."), code=GPGCode.INVALID_HASH)
def validate_key(key, sign=False, encrypt=False): if key.revoked: raise GPGProblem("The key \"" + key.uids[0].uid + "\" is revoked.", code=GPGCode.KEY_REVOKED) elif key.expired: raise GPGProblem("The key \"" + key.uids[0].uid + "\" is expired.", code=GPGCode.KEY_EXPIRED) elif key.invalid: raise GPGProblem("The key \"" + key.uids[0].uid + "\" is invalid.", code=GPGCode.KEY_INVALID) if encrypt and not key.can_encrypt: raise GPGProblem("The key \"" + key.uids[0].uid + "\" can not " + "encrypt.", code=GPGCode.KEY_CANNOT_ENCRYPT) if sign and not key.can_sign: raise GPGProblem("The key \"" + key.uids[0].uid + "\" can not sign.", code=GPGCode.KEY_CANNOT_SIGN)
def test_get_key_fails(self): mail = self.FakeMail() with mock.patch('alot.db.utils.crypto.get_key', mock.Mock(side_effect=GPGProblem(u'', 0))): utils.add_signature_headers(mail, [mock.Mock(fpr='')], u'') self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'False'), mail.headers) self.assertIn((utils.X_SIGNATURE_MESSAGE_HEADER, u'Untrusted: '), mail.headers)
def verify_detached(message, signature): '''Verifies whether the message is authentic by checking the signature. :param message: the message as `str` :param signature: a `str` containing an OpenPGP signature :returns: a list of :class:`gpgme.Signature` :raises: :class:`~alot.errors.GPGProblem` if the verification fails ''' message_data = StringIO(message) signature_data = StringIO(signature) ctx = gpgme.Context() try: return ctx.verify(signature_data, message_data, None) except gpgme.GpgmeError as e: raise GPGProblem(e.message, code=e.code)
def decrypt_verify(encrypted): '''Decrypts the given ciphertext string and returns both the signatures (if any) and the plaintext. :param encrypted: the mail to decrypt :returns: a tuple (sigs, plaintext) with sigs being a list of a :class:`gpgme.Signature` and plaintext is a `str` holding the decrypted mail :raises: :class:`~alot.errors.GPGProblem` if the decryption fails ''' encrypted_data = StringIO(encrypted) plaintext_data = StringIO() ctx = gpgme.Context() try: sigs = ctx.decrypt_verify(encrypted_data, plaintext_data) except gpgme.GpgmeError as e: raise GPGProblem(e.message, code=e.code) plaintext_data.seek(0, os.SEEK_SET) return sigs, plaintext_data.read()
def get_key(keyid): """ Gets a key from the keyring by filtering for the specified keyid, but only if the given keyid is specific enough (if it matches multiple keys, an exception will be thrown). :param keyid: filter term for the keyring (usually a key ID) :rtype: gpgme.Key """ ctx = gpgme.Context() try: key = ctx.get_key(keyid) except gpgme.GpgmeError as e: if e.code == gpgme.ERR_AMBIGUOUS_NAME: # Deferred import to avoid a circular import dependency from alot.db.errors import GPGProblem raise GPGProblem(("More than one key found matching this filter." " Please be more specific (use a key ID like 4AC8EE1D).")) else: raise e return key
class TestSignCommand(unittest.TestCase): """Tests for the SignCommand class.""" @staticmethod def _make_ui_mock(): """Create a mock for the ui and envelope and return them.""" envelope = Envelope() envelope['From'] = 'foo <*****@*****.**>' envelope.sign = mock.sentinel.default envelope.sign_key = mock.sentinel.default ui = utilities.make_ui(current_buffer=mock.Mock(envelope=envelope)) return envelope, ui @mock.patch('alot.commands.envelope.crypto.get_key', mock.Mock(return_value=mock.sentinel.keyid)) def test_apply_keyid_success(self): """If there is a valid keyid then key and to sign should be set. """ env, ui = self._make_ui_mock() # The actual keyid doesn't matter, since it'll be mocked anyway cmd = envelope.SignCommand(action='sign', keyid=['a']) cmd.apply(ui) self.assertTrue(env.sign) self.assertEqual(env.sign_key, mock.sentinel.keyid) @mock.patch('alot.commands.envelope.crypto.get_key', mock.Mock(side_effect=GPGProblem('sentinel', 0))) def test_apply_keyid_gpgproblem(self): """If there is an invalid keyid then the signing key and to sign should be set to false and default. """ env, ui = self._make_ui_mock() # The actual keyid doesn't matter, since it'll be mocked anyway cmd = envelope.SignCommand(action='sign', keyid=['a']) cmd.apply(ui) self.assertFalse(env.sign) self.assertEqual(env.sign_key, mock.sentinel.default) ui.notify.assert_called_once() @mock.patch('alot.commands.envelope.settings.account_matching_address', mock.Mock(side_effect=NoMatchingAccount)) def test_apply_no_keyid_nomatchingaccount(self): """If there is a nokeyid and no account can be found to match the From, then the envelope should not be marked to sign. """ env, ui = self._make_ui_mock() # The actual keyid doesn't matter, since it'll be mocked anyway cmd = envelope.SignCommand(action='sign', keyid=None) cmd.apply(ui) self.assertFalse(env.sign) self.assertEqual(env.sign_key, mock.sentinel.default) ui.notify.assert_called_once() def test_apply_no_keyid_no_gpg_key(self): """If there is a nokeyid and the account has no gpg key then the signing key and to sign should be set to false and default. """ env, ui = self._make_ui_mock() env.account = mock.Mock(gpg_key=None) cmd = envelope.SignCommand(action='sign', keyid=None) cmd.apply(ui) self.assertFalse(env.sign) self.assertEqual(env.sign_key, mock.sentinel.default) ui.notify.assert_called_once() def test_apply_no_keyid_default(self): """If there is no keyid and the account has a gpg key, then that should be used. """ env, ui = self._make_ui_mock() env.account = mock.Mock(gpg_key='sentinel') cmd = envelope.SignCommand(action='sign', keyid=None) cmd.apply(ui) self.assertTrue(env.sign) self.assertEqual(env.sign_key, 'sentinel') @mock.patch('alot.commands.envelope.crypto.get_key', mock.Mock(return_value=mock.sentinel.keyid)) def test_apply_no_sign(self): """If signing with a valid keyid and valid key then set sign and sign_key. """ env, ui = self._make_ui_mock() # The actual keyid doesn't matter, since it'll be mocked anyway cmd = envelope.SignCommand(action='sign', keyid=['a']) cmd.apply(ui) self.assertTrue(env.sign) self.assertEqual(env.sign_key, mock.sentinel.keyid) @mock.patch('alot.commands.envelope.crypto.get_key', mock.Mock(return_value=mock.sentinel.keyid)) def test_apply_unsign(self): """Test that settingun sign sets the sign to False if all other conditions allow for it. """ env, ui = self._make_ui_mock() env.sign = True env.sign_key = mock.sentinel # The actual keyid doesn't matter, since it'll be mocked anyway cmd = envelope.SignCommand(action='unsign', keyid=['a']) cmd.apply(ui) self.assertFalse(env.sign) self.assertIs(env.sign_key, None) @mock.patch('alot.commands.envelope.crypto.get_key', mock.Mock(return_value=mock.sentinel.keyid)) def test_apply_togglesign(self): """Test that toggling changes the sign and sign_key as approriate if other condtiions allow for it """ env, ui = self._make_ui_mock() env.sign = True env.sign_key = mock.sentinel.keyid # The actual keyid doesn't matter, since it'll be mocked anyway # Test that togling from true to false works cmd = envelope.SignCommand(action='toggle', keyid=['a']) cmd.apply(ui) self.assertFalse(env.sign) self.assertIs(env.sign_key, None) # Test that toggling back to True works cmd.apply(ui) self.assertTrue(env.sign) self.assertIs(env.sign_key, mock.sentinel.keyid) def _make_local_settings(self): config = textwrap.dedent("""\ [accounts] [[default]] realname = foo address = [email protected] sendmail_command = /bin/true """) # Allow settings.reload to work by not deleting the file until the end with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(config) self.addCleanup(os.unlink, f.name) # Set the gpg_key separately to avoid validation failures manager = SettingsManager() manager.read_config(f.name) manager.get_accounts()[0].gpg_key = mock.sentinel.gpg_key return manager def test_apply_from_email_only(self): """Test that a key can be derived using a 'From' header that contains only an email. If the from header is in the form "*****@*****.**" and a key exists it should be used. """ manager = self._make_local_settings() env, ui = self._make_ui_mock() env.headers = {'From': ['*****@*****.**']} cmd = envelope.SignCommand(action='sign') with mock.patch('alot.commands.envelope.settings', manager): cmd.apply(ui) self.assertTrue(env.sign) self.assertIs(env.sign_key, mock.sentinel.gpg_key) def test_apply_from_user_and_email(self): """This tests that a gpg key can be derived using a 'From' header that contains a realname-email combo. If the header is in the form "Foo <*****@*****.**>", a key should be derived. See issue #1113 """ manager = self._make_local_settings() env, ui = self._make_ui_mock() cmd = envelope.SignCommand(action='sign') with mock.patch('alot.commands.envelope.settings', manager): cmd.apply(ui) self.assertTrue(env.sign) self.assertIs(env.sign_key, mock.sentinel.gpg_key)
def construct_mail(self): """ compiles the information contained in this envelope into a :class:`email.Message`. """ # Build body text part. To properly sign/encrypt messages later on, we # convert the text to its canonical format (as per RFC 2015). canonical_format = self.body.encode('utf-8') canonical_format = canonical_format.replace('\\t', ' ' * 4) textpart = MIMEText(canonical_format, 'plain', 'utf-8') # wrap it in a multipart container if necessary if self.attachments: inner_msg = MIMEMultipart() inner_msg.attach(textpart) # add attachments for a in self.attachments: inner_msg.attach(a.get_mime_representation()) else: inner_msg = textpart if self.sign: plaintext = helper.email_as_string(inner_msg) logging.debug('signing plaintext: ' + plaintext) try: signatures, signature_str = crypto.detached_signature_for( plaintext, self.sign_key) if len(signatures) != 1: raise GPGProblem("Could not sign message (GPGME " "did not return a signature)", code=GPGCode.KEY_CANNOT_SIGN) except gpgme.GpgmeError as e: if e.code == gpgme.ERR_BAD_PASSPHRASE: # If GPG_AGENT_INFO is unset or empty, the user just does # not have gpg-agent running (properly). if os.environ.get('GPG_AGENT_INFO', '').strip() == '': msg = "Got invalid passphrase and GPG_AGENT_INFO\ not set. Please set up gpg-agent." raise GPGProblem(msg, code=GPGCode.BAD_PASSPHRASE) else: raise GPGProblem("Bad passphrase. Is gpg-agent " "running?", code=GPGCode.BAD_PASSPHRASE) raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_SIGN) micalg = crypto.RFC3156_micalg_from_algo(signatures[0].hash_algo) unencrypted_msg = MIMEMultipart('signed', micalg=micalg, protocol='application/pgp-signature') # wrap signature in MIMEcontainter stype = 'pgp-signature; name="signature.asc"' signature_mime = MIMEApplication(_data=signature_str, _subtype=stype, _encoder=encode_7or8bit) signature_mime['Content-Description'] = 'signature' signature_mime.set_charset('us-ascii') # add signed message and signature to outer message unencrypted_msg.attach(inner_msg) unencrypted_msg.attach(signature_mime) unencrypted_msg['Content-Disposition'] = 'inline' else: unencrypted_msg = inner_msg if self.encrypt: plaintext = helper.email_as_string(unencrypted_msg) logging.debug('encrypting plaintext: ' + plaintext) try: encrypted_str = crypto.encrypt(plaintext, self.encrypt_keys.values()) except gpgme.GpgmeError as e: raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_ENCRYPT) outer_msg = MIMEMultipart('encrypted', protocol='application/pgp-encrypted') version_str = 'Version: 1' encryption_mime = MIMEApplication(_data=version_str, _subtype='pgp-encrypted', _encoder=encode_7or8bit) encryption_mime.set_charset('us-ascii') encrypted_mime = MIMEApplication(_data=encrypted_str, _subtype='octet-stream', _encoder=encode_7or8bit) encrypted_mime.set_charset('us-ascii') outer_msg.attach(encryption_mime) outer_msg.attach(encrypted_mime) else: outer_msg = unencrypted_msg headers = self.headers.copy() # add Message-ID if 'Message-ID' not in headers: headers['Message-ID'] = [email.Utils.make_msgid()] if 'User-Agent' in headers: uastring_format = headers['User-Agent'][0] else: uastring_format = settings.get('user_agent').strip() uastring = uastring_format.format(version=__version__) if uastring: headers['User-Agent'] = [uastring] # copy headers from envelope to mail for k, vlist in headers.items(): for v in vlist: outer_msg[k] = encode_header(k, v) return outer_msg