Example #1
0
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)
Example #2
0
File: crypto.py Project: xunam/alot
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
Example #3
0
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
Example #4
0
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)
Example #5
0
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)
Example #6
0
 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)
Example #7
0
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)
Example #8
0
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()
Example #9
0
File: crypto.py Project: t-8ch/alot
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
Example #10
0
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)
Example #11
0
    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