diff --git a/plugins/GPG/README.md b/plugins/GPG/README.md new file mode 100644 index 000000000..c72b4615d --- /dev/null +++ b/plugins/GPG/README.md @@ -0,0 +1 @@ +Provides authentication based on GPG keys. diff --git a/plugins/GPG/__init__.py b/plugins/GPG/__init__.py new file mode 100644 index 000000000..9ff5de97b --- /dev/null +++ b/plugins/GPG/__init__.py @@ -0,0 +1,68 @@ +### +# Copyright (c) 2015, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +""" +GPG: Provides authentication based on GPG keys. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.authors.unknown + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = '' + +from . import config +from . import plugin +from imp import reload +# In case we're being reloaded. +reload(config) +reload(plugin) +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + from . import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/GPG/config.py b/plugins/GPG/config.py new file mode 100644 index 000000000..8c374ba12 --- /dev/null +++ b/plugins/GPG/config.py @@ -0,0 +1,57 @@ +### +# Copyright (c) 2015, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +import supybot.conf as conf +import supybot.registry as registry +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('GPG') +except: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified themself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('GPG', True) + + +GPG = conf.registerPlugin('GPG') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(GPG, 'someConfigVariableName', +# registry.Boolean(False, _("""Help for someConfigVariableName."""))) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/GPG/local/__init__.py b/plugins/GPG/local/__init__.py new file mode 100644 index 000000000..e86e97b86 --- /dev/null +++ b/plugins/GPG/local/__init__.py @@ -0,0 +1 @@ +# Stub so local is a module, used for third-party modules diff --git a/plugins/GPG/plugin.py b/plugins/GPG/plugin.py new file mode 100644 index 000000000..d6dff5a7f --- /dev/null +++ b/plugins/GPG/plugin.py @@ -0,0 +1,184 @@ +### +# Copyright (c) 2015, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +import re + +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('GPG') +except ImportError: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + + +class GPG(callbacks.Plugin): + """Provides authentication based on GPG keys.""" + class key(callbacks.Commands): + def add(self, irc, msg, args, user, keyid, keyserver): + """ + + Add a GPG key to your account.""" + if keyid in user.gpgkeys: + irc.error(_('This key is already associated with your ' + 'account.')) + return + result = gpg.keyring.recv_keys(keyserver, keyid) + reply = format(_('%n imported, %i unchanged, %i not imported.'), + (result.imported, _('key')), + result.unchanged, + result.not_imported, + [x['fingerprint'] for x in result.results]) + if result.imported == 1: + user.gpgkeys.append(keyid) + irc.reply(reply) + else: + irc.error(reply) + add = wrap(add, ['user', + ('somethingWithoutSpaces', + _('You must give a valid key id')), + ('somethingWithoutSpaces', + _('You must give a valid key server'))]) + + def remove(self, irc, msg, args, user, fingerprint): + """ + + Remove a GPG key from your account.""" + try: + keyids = [x['keyid'] for x in gpg.keyring.list_keys() + if x['fingerprint'] == fingerprint] + if len(keyids) == 0: + raise ValueError + for keyid in keyids: + try: + user.gpgkeys.remove(keyid) + except ValueError: + user.gpgkeys.remove('0x' + keyid) + gpg.keyring.delete_keys(fingerprint) + irc.replySuccess() + except ValueError: + irc.error(_('GPG key not associated with your account.')) + remove = wrap(remove, ['user', 'somethingWithoutSpaces']) + + def list(self, irc, msg, args, user): + """takes no arguments + + List your GPG keys.""" + keyids = user.gpgkeys + if len(keyids) == 0: + irc.reply(_('No key is associated with your account.')) + else: + irc.reply(format('%L', keyids)) + list = wrap(list, ['user']) + + class sign(callbacks.Commands): + def __init__(self, *args): + super(User.gpg, self).__init__(*args) + self._tokens = {} + + def _expire_tokens(self): + now = time.time() + self._tokens = dict(filter(lambda x_y: x_y[1][1]>now, + self._tokens.items())) + + def gettoken(self, irc, msg, args): + """takes no arguments + + Send you a token that you'll have to sign with your key.""" + self._expire_tokens() + token = '{%s}' % str(uuid.uuid4()) + lifetime = conf.supybot.plugins.User.gpg.TokenTimeout() + self._tokens.update({token: (msg.prefix, time.time()+lifetime)}) + irc.reply(_('Your token is: %s. Please sign it with your ' + 'GPG key, paste it somewhere, and call the \'auth\' ' + 'command with the URL to the (raw) file containing the ' + 'signature.') % token) + gettoken = wrap(gettoken, []) + + _auth_re = re.compile(r'-----BEGIN PGP SIGNED MESSAGE-----\r?\n' + r'Hash: .*\r?\n\r?\n' + r'\s*({[0-9a-z-]+})\s*\r?\n' + r'-----BEGIN PGP SIGNATURE-----\r?\n.*' + r'\r?\n-----END PGP SIGNATURE-----', + re.S) + + def auth(self, irc, msg, args, url): + """ + + Check the GPG signature at the and authenticates you if + the key used is associated to a user.""" + self._expire_tokens() + content = utils.web.getUrl(url) + if sys.version_info[0] >= 3 and isinstance(content, bytes): + content = content.decode() + match = self._auth_re.search(content) + if not match: + irc.error(_('Signature or token not found.'), Raise=True) + data = match.group(0) + token = match.group(1) + if token not in self._tokens: + irc.error(_('Unknown token. It may have expired before you ' + 'submit it.'), Raise=True) + if self._tokens[token][0] != msg.prefix: + irc.error(_('Your hostname/nick changed in the process. ' + 'Authentication aborted.'), Raise=True) + verified = gpg.keyring.verify(data) + if verified and verified.valid: + keyid = verified.pubkey_fingerprint[-16:] + prefix, expiry = self._tokens.pop(token) + found = False + for (id, user) in ircdb.users.items(): + if keyid in [x[-len(keyid):] for x in user.gpgkeys]: + try: + user.addAuth(msg.prefix) + except ValueError: + irc.error(_('Your secure flag is true and your ' + 'hostmask doesn\'t match any of your ' + 'known hostmasks.'), Raise=True) + ircdb.users.setUser(user, flush=False) + irc.reply(_('You are now authenticated as %s.') % + user.name) + return + irc.error(_('Unknown GPG key.'), Raise=True) + else: + irc.error(_('Signature could not be verified. Make sure ' + 'this is a valid GPG signature and the URL is valid.')) + auth = wrap(auth, ['url']) + + +Class = GPG + + +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/plugins/GPG/test.py b/plugins/GPG/test.py new file mode 100644 index 000000000..eaaaaf82f --- /dev/null +++ b/plugins/GPG/test.py @@ -0,0 +1,139 @@ +### +# Copyright (c) 2015, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +from supybot.test import * + +import supybot.gpg as gpg + +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +lQHYBFD7GxQBBACeu7bj/wgnnv5NkfHImZJVJLaq2cwKYc3rErv7pqLXpxXZbDOI +jP+5eSmTLhPUK67aRD6gG0wQ9iAhYR03weOmyjDGh0eF7kLYhu/4Il56Y/YbB8ll +Imz/pep/Hi72ShcW8AtifDup/KeHjaWa1yF2WThHbX/0N2ghSxbJnatpBwARAQAB +AAP6Arf7le7FD3ZhGZvIBkPr25qca6i0Qxb5XpOinV7jLcoycZriJ9Xofmhda9UO +xhNVppMvs/ofI/m0umnR4GLKtRKnJSc8Edxi4YKyqLehfBTF20R/kBYPZ772FkNW +Kzo5yCpP1jpOc0+QqBuU7OmrG4QhQzTLXIUgw4XheORncEECAMGkvR47PslJqzbY +VRIzWEv297r1Jxqy6qgcuCJn3RWYJbEZ/qdTYy+MgHGmaNFQ7yhfIzkBueq0RWZp +Z4PfJn8CANHZGj6AJZcvb+VclNtc5VNfnKjYD+qQOh2IS8NhE/0umGMKz3frH1TH +yCbh2LlPR89cqNcd4QvbHKA/UmzISXkB/37MbUnxXTpS9Y4HNpQCh/6SYlB0lucV +QN0cgjfhd6nBrb6uO6+u40nBzgynWcEpPMNfN0AtQeA4Dx+WrnK6kZqfd7QMU3Vw +eWJvdCB0ZXN0iLgEEwECACIFAlD7GxQCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B +AheAAAoJEMnTMjwgrwErV3AD/0kRq8UWPlkc6nyiIR6qiT3EoBNHKIi4cz68Wa1u +F2M6einrRR0HolrxonynTGsdr1u2f3egOS4fNfGhTNAowSefYR9q5kIYiYE2DL5G +YnjJKNfmnRxZM9YqmEnN50rgu2cifSRehp61fXdTtmOAR3js+9wb73dwbYzr3kIc +3WH1 +=UBcd +-----END PGP PRIVATE KEY BLOCK----- +""" + +WRONG_TOKEN_SIGNATURE = """ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +{a95dc112-780e-47f7-a83a-c6f3820d7dc3} +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +iJwEAQECAAYFAlD7Jb0ACgkQydMyPCCvASv9HgQAhQf/oFMWcKwGncH0hjXC3QYz +7ck3chgL3S1pPAvS69viz6i2bwYZYD8fhzHNJ/qtw/rx6thO6PwT4SpdhKerap+I +kdem3LjM4fAGHRunHZYP39obNKMn1xv+f26mEAAWxdv/W/BLAFqxi3RijJywRkXm +zo5GUl844kpnV+uk0Xk= +=z2Cz +-----END PGP SIGNATURE----- +""" + +FINGERPRINT = '2CF3E41500218D30F0B654F5C9D3323C20AF012B' + + +class GPGTestCase(PluginTestCase): + plugins = ('GPG',) + + def setUp(self): + super(GPGTestCase, self).setUp() + gpg.loadKeyring() + + if gpg.available and network: + def testGpgAddRemove(self): + self.assertNotError('register foo bar') + self.assertError('gpg key add 51E516F0B0C5CE6A pgp.mit.edu') + self.assertResponse('gpg key add EB17F1E0CEB63930 pgp.mit.edu', + '1 key imported, 0 unchanged, 0 not imported.') + self.assertNotError( + 'gpg key remove F88ECDE235846FA8652DAF5FEB17F1E0CEB63930') + self.assertResponse('gpg key add EB17F1E0CEB63930 pgp.mit.edu', + '1 key imported, 0 unchanged, 0 not imported.') + self.assertResponse('gpg key add EB17F1E0CEB63930 pgp.mit.edu', + 'Error: This key is already associated with your account.') + + if gpg.available: + def testGpgAuth(self): + self.assertNotError('register spam egg') + gpg.keyring.import_keys(PRIVATE_KEY).__dict__ + (id, user) = ircdb.users.items()[0] + user.gpgkeys.append(FINGERPRINT) + msg = self.getMsg('gpg gettoken').args[-1] + match = re.search('is: ({.*}).', msg) + assert match, repr(msg) + token = match.group(1) + + def fakeGetUrlFd(*args, **kwargs): + return fd + (utils.web.getUrlFd, realGetUrlFd) = (fakeGetUrlFd, utils.web.getUrlFd) + + fd = StringIO() + fd.write('foo') + fd.seek(0) + self.assertResponse('gpg sign auth http://foo.bar/baz.gpg', + 'Error: Signature or token not found.') + + fd = StringIO() + fd.write(token) + fd.seek(0) + self.assertResponse('gpg sign auth http://foo.bar/baz.gpg', + 'Error: Signature or token not found.') + + fd = StringIO() + fd.write(WRONG_TOKEN_SIGNATURE) + fd.seek(0) + self.assertRegexp('gpg sign auth http://foo.bar/baz.gpg', + 'Error: Unknown token.*') + + fd = StringIO() + fd.write(str(gpg.keyring.sign(token))) + fd.seek(0) + self.assertResponse('gpg sign auth http://foo.bar/baz.gpg', + 'You are now authenticated as spam.') + + utils.web.getUrlFd = realGetUrlFd + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugins/User/plugin.py b/plugins/User/plugin.py index 39a03a089..6f998bdaf 100644 --- a/plugins/User/plugin.py +++ b/plugins/User/plugin.py @@ -410,140 +410,6 @@ class User(callbacks.Plugin): else: irc.error(_('GPG features are not enabled.')) - class gpg(callbacks.Commands): - def __init__(self, *args): - super(User.gpg, self).__init__(*args) - self._tokens = {} - - def _expire_tokens(self): - now = time.time() - self._tokens = dict(filter(lambda x_y: x_y[1][1]>now, - self._tokens.items())) - - @internationalizeDocstring - def add(self, irc, msg, args, user, keyid, keyserver): - """ - - Add a GPG key to your account.""" - if keyid in user.gpgkeys: - irc.error(_('This key is already associated with your ' - 'account.')) - return - result = gpg.keyring.recv_keys(keyserver, keyid) - reply = format(_('%n imported, %i unchanged, %i not imported.'), - (result.imported, _('key')), - result.unchanged, - result.not_imported, - [x['fingerprint'] for x in result.results]) - if result.imported == 1: - user.gpgkeys.append(keyid) - irc.reply(reply) - else: - irc.error(reply) - add = wrap(add, ['user', - ('somethingWithoutSpaces', - _('You must give a valid key id')), - ('somethingWithoutSpaces', - _('You must give a valid key server'))]) - - @internationalizeDocstring - def remove(self, irc, msg, args, user, fingerprint): - """ - - Remove a GPG key from your account.""" - try: - keyids = [x['keyid'] for x in gpg.keyring.list_keys() - if x['fingerprint'] == fingerprint] - if len(keyids) == 0: - raise ValueError - for keyid in keyids: - try: - user.gpgkeys.remove(keyid) - except ValueError: - user.gpgkeys.remove('0x' + keyid) - gpg.keyring.delete_keys(fingerprint) - irc.replySuccess() - except ValueError: - irc.error(_('GPG key not associated with your account.')) - remove = wrap(remove, ['user', 'somethingWithoutSpaces']) - - @internationalizeDocstring - def list(self, irc, msg, args, user): - """takes no arguments - - List your GPG keys.""" - keyids = user.gpgkeys - if len(keyids) == 0: - irc.reply(_('No key is associated with your account.')) - else: - irc.reply(format('%L', keyids)) - list = wrap(list, ['user']) - - @internationalizeDocstring - def gettoken(self, irc, msg, args): - """takes no arguments - - Send you a token that you'll have to sign with your key.""" - self._expire_tokens() - token = '{%s}' % str(uuid.uuid4()) - lifetime = conf.supybot.plugins.User.gpg.TokenTimeout() - self._tokens.update({token: (msg.prefix, time.time()+lifetime)}) - irc.reply(_('Your token is: %s. Please sign it with your ' - 'GPG key, paste it somewhere, and call the \'auth\' ' - 'command with the URL to the (raw) file containing the ' - 'signature.') % token) - gettoken = wrap(gettoken, []) - - _auth_re = re.compile(r'-----BEGIN PGP SIGNED MESSAGE-----\r?\n' - r'Hash: .*\r?\n\r?\n' - r'\s*({[0-9a-z-]+})\s*\r?\n' - r'-----BEGIN PGP SIGNATURE-----\r?\n.*' - r'\r?\n-----END PGP SIGNATURE-----', - re.S) - - @internationalizeDocstring - def auth(self, irc, msg, args, url): - """ - - Check the GPG signature at the and authenticates you if - the key used is associated to a user.""" - self._expire_tokens() - content = utils.web.getUrl(url) - if sys.version_info[0] >= 3 and isinstance(content, bytes): - content = content.decode() - match = self._auth_re.search(content) - if not match: - irc.error(_('Signature or token not found.'), Raise=True) - data = match.group(0) - token = match.group(1) - if token not in self._tokens: - irc.error(_('Unknown token. It may have expired before you ' - 'submit it.'), Raise=True) - if self._tokens[token][0] != msg.prefix: - irc.error(_('Your hostname/nick changed in the process. ' - 'Authentication aborted.'), Raise=True) - verified = gpg.keyring.verify(data) - if verified and verified.valid: - keyid = verified.pubkey_fingerprint[-16:] - prefix, expiry = self._tokens.pop(token) - found = False - for (id, user) in ircdb.users.items(): - if keyid in [x[-len(keyid):] for x in user.gpgkeys]: - try: - user.addAuth(msg.prefix) - except ValueError: - irc.error(_('Your secure flag is true and your ' - 'hostmask doesn\'t match any of your ' - 'known hostmasks.'), Raise=True) - ircdb.users.setUser(user, flush=False) - irc.reply(_('You are now authenticated as %s.') % - user.name) - return - irc.error(_('Unknown GPG key.'), Raise=True) - else: - irc.error(_('Signature could not be verified. Make sure ' - 'this is a valid GPG signature and the URL is valid.')) - auth = wrap(auth, ['url']) @internationalizeDocstring def capabilities(self, irc, msg, args, user): diff --git a/plugins/User/test.py b/plugins/User/test.py index 1a04000b8..038dfd16c 100644 --- a/plugins/User/test.py +++ b/plugins/User/test.py @@ -30,7 +30,6 @@ import re from cStringIO import StringIO -import supybot.gpg as gpg from supybot.test import PluginTestCase, network import supybot.conf as conf @@ -38,56 +37,11 @@ import supybot.world as world import supybot.ircdb as ircdb import supybot.utils as utils -PRIVATE_KEY = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.12 (GNU/Linux) - -lQHYBFD7GxQBBACeu7bj/wgnnv5NkfHImZJVJLaq2cwKYc3rErv7pqLXpxXZbDOI -jP+5eSmTLhPUK67aRD6gG0wQ9iAhYR03weOmyjDGh0eF7kLYhu/4Il56Y/YbB8ll -Imz/pep/Hi72ShcW8AtifDup/KeHjaWa1yF2WThHbX/0N2ghSxbJnatpBwARAQAB -AAP6Arf7le7FD3ZhGZvIBkPr25qca6i0Qxb5XpOinV7jLcoycZriJ9Xofmhda9UO -xhNVppMvs/ofI/m0umnR4GLKtRKnJSc8Edxi4YKyqLehfBTF20R/kBYPZ772FkNW -Kzo5yCpP1jpOc0+QqBuU7OmrG4QhQzTLXIUgw4XheORncEECAMGkvR47PslJqzbY -VRIzWEv297r1Jxqy6qgcuCJn3RWYJbEZ/qdTYy+MgHGmaNFQ7yhfIzkBueq0RWZp -Z4PfJn8CANHZGj6AJZcvb+VclNtc5VNfnKjYD+qQOh2IS8NhE/0umGMKz3frH1TH -yCbh2LlPR89cqNcd4QvbHKA/UmzISXkB/37MbUnxXTpS9Y4HNpQCh/6SYlB0lucV -QN0cgjfhd6nBrb6uO6+u40nBzgynWcEpPMNfN0AtQeA4Dx+WrnK6kZqfd7QMU3Vw -eWJvdCB0ZXN0iLgEEwECACIFAlD7GxQCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B -AheAAAoJEMnTMjwgrwErV3AD/0kRq8UWPlkc6nyiIR6qiT3EoBNHKIi4cz68Wa1u -F2M6einrRR0HolrxonynTGsdr1u2f3egOS4fNfGhTNAowSefYR9q5kIYiYE2DL5G -YnjJKNfmnRxZM9YqmEnN50rgu2cifSRehp61fXdTtmOAR3js+9wb73dwbYzr3kIc -3WH1 -=UBcd ------END PGP PRIVATE KEY BLOCK----- -""" - -WRONG_TOKEN_SIGNATURE = """ ------BEGIN PGP SIGNED MESSAGE----- -Hash: SHA1 - -{a95dc112-780e-47f7-a83a-c6f3820d7dc3} ------BEGIN PGP SIGNATURE----- -Version: GnuPG v1.4.12 (GNU/Linux) - -iJwEAQECAAYFAlD7Jb0ACgkQydMyPCCvASv9HgQAhQf/oFMWcKwGncH0hjXC3QYz -7ck3chgL3S1pPAvS69viz6i2bwYZYD8fhzHNJ/qtw/rx6thO6PwT4SpdhKerap+I -kdem3LjM4fAGHRunHZYP39obNKMn1xv+f26mEAAWxdv/W/BLAFqxi3RijJywRkXm -zo5GUl844kpnV+uk0Xk= -=z2Cz ------END PGP SIGNATURE----- -""" - -FINGERPRINT = '2CF3E41500218D30F0B654F5C9D3323C20AF012B' - class UserTestCase(PluginTestCase): plugins = ('User', 'Admin', 'Config') prefix1 = 'somethingElse!user@host.tld' prefix2 = 'EvensomethingElse!user@host.tld' - def setUp(self): - super(UserTestCase, self).setUp() - gpg.loadKeyring() - def testHostmaskList(self): self.assertError('hostmask list') original = self.prefix @@ -203,59 +157,5 @@ class UserTestCase(PluginTestCase): self.assertNotError('load Seen') self.assertResponse('user list', 'Foo') - if gpg.available and network: - def testGpgAddRemove(self): - self.assertNotError('register foo bar') - self.assertError('user gpg add 51E516F0B0C5CE6A pgp.mit.edu') - self.assertResponse('user gpg add EB17F1E0CEB63930 pgp.mit.edu', - '1 key imported, 0 unchanged, 0 not imported.') - self.assertNotError( - 'user gpg remove F88ECDE235846FA8652DAF5FEB17F1E0CEB63930') - self.assertResponse('user gpg add EB17F1E0CEB63930 pgp.mit.edu', - '1 key imported, 0 unchanged, 0 not imported.') - self.assertResponse('user gpg add EB17F1E0CEB63930 pgp.mit.edu', - 'Error: This key is already associated with your account.') - - if gpg.available: - def testGpgAuth(self): - self.assertNotError('register spam egg') - gpg.keyring.import_keys(PRIVATE_KEY).__dict__ - (id, user) = ircdb.users.items()[0] - user.gpgkeys.append(FINGERPRINT) - msg = self.getMsg('gpg gettoken').args[-1] - match = re.search('is: ({.*}).', msg) - assert match, repr(msg) - token = match.group(1) - - def fakeGetUrlFd(*args, **kwargs): - return fd - (utils.web.getUrlFd, realGetUrlFd) = (fakeGetUrlFd, utils.web.getUrlFd) - - fd = StringIO() - fd.write('foo') - fd.seek(0) - self.assertResponse('gpg auth http://foo.bar/baz.gpg', - 'Error: Signature or token not found.') - - fd = StringIO() - fd.write(token) - fd.seek(0) - self.assertResponse('gpg auth http://foo.bar/baz.gpg', - 'Error: Signature or token not found.') - - fd = StringIO() - fd.write(WRONG_TOKEN_SIGNATURE) - fd.seek(0) - self.assertRegexp('gpg auth http://foo.bar/baz.gpg', - 'Error: Unknown token.*') - - fd = StringIO() - fd.write(str(gpg.keyring.sign(token))) - fd.seek(0) - self.assertResponse('gpg auth http://foo.bar/baz.gpg', - 'You are now authenticated as spam.') - - utils.web.getUrlFd = realGetUrlFd - # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: