From 4997d5c13d5dfdf3978c1f9288ff8d3f35ad69df Mon Sep 17 00:00:00 2001 From: wubijie Date: Tue, 7 Feb 2023 15:54:24 +0800 Subject: [PATCH] Update package to version 1.6.2 --- python-yubico-1.3.3.tar.gz | Bin 40900 -> 0 bytes python-yubico.spec | 11 +- yubico-1.6.2.tar.gz/LICENSE | 24 ++ yubico-1.6.2.tar.gz/MANIFEST.in | 5 + yubico-1.6.2.tar.gz/NOTICE | 2 + yubico-1.6.2.tar.gz/PKG-INFO | 21 + yubico-1.6.2.tar.gz/README.md | 65 +++ yubico-1.6.2.tar.gz/setup.cfg | 5 + yubico-1.6.2.tar.gz/setup.py | 105 +++++ yubico-1.6.2.tar.gz/tests/__init__.py | 0 yubico-1.6.2.tar.gz/tests/mock_http_server.py | 108 +++++ yubico-1.6.2.tar.gz/tests/test_yubico.py | 152 +++++++ yubico-1.6.2.tar.gz/tests/utils.py | 64 +++ yubico-1.6.2.tar.gz/yubico.egg-info/PKG-INFO | 21 + .../yubico.egg-info/SOURCES.txt | 19 + .../yubico.egg-info/dependency_links.txt | 1 + .../yubico.egg-info/requires.txt | 1 + .../yubico.egg-info/top_level.txt | 1 + yubico-1.6.2.tar.gz/yubico/__init__.py | 1 + yubico-1.6.2.tar.gz/yubico/modhex.py | 149 +++++++ yubico-1.6.2.tar.gz/yubico/otp.py | 39 ++ yubico-1.6.2.tar.gz/yubico/yubico.py | 374 ++++++++++++++++++ .../yubico/yubico_exceptions.py | 51 +++ 23 files changed, 1215 insertions(+), 4 deletions(-) delete mode 100644 python-yubico-1.3.3.tar.gz create mode 100644 yubico-1.6.2.tar.gz/LICENSE create mode 100644 yubico-1.6.2.tar.gz/MANIFEST.in create mode 100644 yubico-1.6.2.tar.gz/NOTICE create mode 100644 yubico-1.6.2.tar.gz/PKG-INFO create mode 100644 yubico-1.6.2.tar.gz/README.md create mode 100644 yubico-1.6.2.tar.gz/setup.cfg create mode 100644 yubico-1.6.2.tar.gz/setup.py create mode 100644 yubico-1.6.2.tar.gz/tests/__init__.py create mode 100755 yubico-1.6.2.tar.gz/tests/mock_http_server.py create mode 100644 yubico-1.6.2.tar.gz/tests/test_yubico.py create mode 100644 yubico-1.6.2.tar.gz/tests/utils.py create mode 100644 yubico-1.6.2.tar.gz/yubico.egg-info/PKG-INFO create mode 100644 yubico-1.6.2.tar.gz/yubico.egg-info/SOURCES.txt create mode 100644 yubico-1.6.2.tar.gz/yubico.egg-info/dependency_links.txt create mode 100644 yubico-1.6.2.tar.gz/yubico.egg-info/requires.txt create mode 100644 yubico-1.6.2.tar.gz/yubico.egg-info/top_level.txt create mode 100644 yubico-1.6.2.tar.gz/yubico/__init__.py create mode 100644 yubico-1.6.2.tar.gz/yubico/modhex.py create mode 100644 yubico-1.6.2.tar.gz/yubico/otp.py create mode 100644 yubico-1.6.2.tar.gz/yubico/yubico.py create mode 100644 yubico-1.6.2.tar.gz/yubico/yubico_exceptions.py diff --git a/python-yubico-1.3.3.tar.gz b/python-yubico-1.3.3.tar.gz deleted file mode 100644 index 04d49ef63fbdbd4b90c277f02dcded144d85d6c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40900 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≪OE- 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ΞeTg&Ysl^T7$NOvoP?9oF_# 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 diff --git a/python-yubico.spec b/python-yubico.spec index ae0d02e..6bafe53 100644 --- a/python-yubico.spec +++ b/python-yubico.spec @@ -1,12 +1,12 @@ %{!?_licensedir:%global license %%doc} Name: python-yubico -Version: 1.3.3 -Release: 3 +Version: 1.6.2 +Release: 1 Summary: Python package for talking to YubiKeys License: BSD-2-Clause URL: https://github.com/Yubico/python-yubico -Source0: https://github.com/Yubico/python-yubico/archive/python-yubico-%{version}.tar.gz +Source0: https://files.pythonhosted.org/packages/43/1e/34093ca0f3d956cfb26cc59d42de9dab14547ec497d43fb7cf2669ca1034/yubico-1.6.2.tar.gz BuildArch: noarch BuildRequires: python3-devel python3-setuptools python3-pytest python3-pyusb @@ -30,7 +30,7 @@ Summary: Docs for python3-yubico Docs for python-yubico %prep -%autosetup -n python-yubico-python-yubico-%{version} -p1 +%autosetup -n yubico-%{version} -p1 %build %py3_build @@ -49,6 +49,9 @@ Docs for python-yubico %doc NEWS README %changelog +* Tue Feb 7 2023 wubijie - 1.6.2-1 +- Update package to version 1.6.2 + * Mon Aug 01 2022 liukuo - 1.3.3-3 - License compliance rectification diff --git a/yubico-1.6.2.tar.gz/LICENSE b/yubico-1.6.2.tar.gz/LICENSE new file mode 100644 index 0000000..c034025 --- /dev/null +++ b/yubico-1.6.2.tar.gz/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2010, Tomaz Muraus +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 nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 Tomaz Muraus 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. diff --git a/yubico-1.6.2.tar.gz/MANIFEST.in b/yubico-1.6.2.tar.gz/MANIFEST.in new file mode 100644 index 0000000..3925e5d --- /dev/null +++ b/yubico-1.6.2.tar.gz/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include NOTICE +include CHANGES +include README.md +include tests/*.py diff --git a/yubico-1.6.2.tar.gz/NOTICE b/yubico-1.6.2.tar.gz/NOTICE new file mode 100644 index 0000000..2317236 --- /dev/null +++ b/yubico-1.6.2.tar.gz/NOTICE @@ -0,0 +1,2 @@ +- yubico/modhex.py - licensed under MIT license and Copyright (c) 2009 + Daniel Holth diff --git a/yubico-1.6.2.tar.gz/PKG-INFO b/yubico-1.6.2.tar.gz/PKG-INFO new file mode 100644 index 0000000..8ec90e4 --- /dev/null +++ b/yubico-1.6.2.tar.gz/PKG-INFO @@ -0,0 +1,21 @@ +Metadata-Version: 1.1 +Name: yubico +Version: 1.6.2 +Summary: Python Yubico Client +Home-page: http://github.com/Kami/python-yubico-client/ +Author: Tomaz Muraus +Author-email: tomaz+pypi@tomaz.me +License: BSD +Download-URL: http://github.com/Kami/python-yubico-client/downloads/ +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Security +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Provides: yubico diff --git a/yubico-1.6.2.tar.gz/README.md b/yubico-1.6.2.tar.gz/README.md new file mode 100644 index 0000000..5433b9b --- /dev/null +++ b/yubico-1.6.2.tar.gz/README.md @@ -0,0 +1,65 @@ +# Yubico Python Client + +Python class for verifying Yubico One Time Passwords (OTPs) based on the +validation protocol version 2.0. + +* Yubico website: [http://www.yubico.com][1] +* Yubico documentation: [http://www.yubico.com/developers/intro/][2] +* Validation Protocol Version 2.0 FAQ: [http://www.yubico.com/develop/open-source-software/web-api-clients/server/][3] +* Validation Protocol Version 2.0 description: [https://github.com/Yubico/yubikey-val/wiki/ValidationProtocolV20][4] + +## Installation + +`pip install yubico` + +## Build Status + +[![Build Status](https://secure.travis-ci.org/Kami/python-yubico-client.png)](http://travis-ci.org/Kami/python-yubico-client) + +## Running Tests + +`python setup.py test` + +## Usage + +1. Generate your client id and secret key (this can be done by visiting the + [Yubico website](https://api.yubico.com/get-api-key/)) +2. Use the client + +Single mode: + + from yubico.yubico import Yubico + + yubico = Yubico('client id', 'secret key') + yubico.verify('otp') + +Multi mode: + + from yubico.yubico import Yubico + + yubico = Yubico('client id', 'secret key') + yubico.verify_multi(['otp 1', 'otp 2', 'otp 3']) + +The **verify** method will return `True` if the provided OTP is valid +(STATUS=OK). + +The **verify_multi** method will return `True` if all of the provided OTPs are +valid (STATUS=OK). + +Both methods can also throw one of the following exceptions: + +- **StatusCodeError** - server returned **REPLAYED_OTP** status code +- **SignatureVerificationError** - server response message signature + verification failed +- **InvalidClientIdError** - client with the specified id does not exist + (server returned **NO_SUCH_CLIENT** status code) +- **Exception** - server returned one of the following status values: + **BAD_OTP**, **BAD_SIGNATURE**, **MISSING_PARAMETER**, + **OPERATION_NOT_ALLOWED**, **BACKEND_ERROR**, **NOT_ENOUGH_ANSWERS**, + **REPLAYED_REQUEST** or no response was received from any of the servers + in the specified time frame (default timeout = 10 seconds) + +[1]: http://www.yubico.com +[2]: http://www.yubico.com/developers/intro/ +[3]: http://www.yubico.com/develop/open-source-software/web-api-clients/server/ +[4]: https://github.com/Yubico/yubikey-val/wiki/ValidationProtocolV20 diff --git a/yubico-1.6.2.tar.gz/setup.cfg b/yubico-1.6.2.tar.gz/setup.cfg new file mode 100644 index 0000000..861a9f5 --- /dev/null +++ b/yubico-1.6.2.tar.gz/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/yubico-1.6.2.tar.gz/setup.py b/yubico-1.6.2.tar.gz/setup.py new file mode 100644 index 0000000..b5c8492 --- /dev/null +++ b/yubico-1.6.2.tar.gz/setup.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import re +import sys +import logging + +from glob import glob +from os.path import splitext, basename, join as pjoin +from unittest import TextTestRunner, TestLoader + +from setuptools import setup +from distutils.core import Command + +sys.path.insert(0, pjoin(os.path.dirname(__file__))) +from tests.utils import MockAPIServerRunner + +TEST_PATHS = ['tests'] + +version_re = re.compile( + r'__version__ = (\(.*?\))') + +cwd = os.path.dirname(os.path.abspath(__file__)) +fp = open(os.path.join(cwd, 'yubico', '__init__.py')) + +version = None +for line in fp: + match = version_re.search(line) + if match: + version = eval(match.group(1)) + break +else: + raise Exception('Cannot find version in __init__.py') +fp.close() + + +class TestCommand(Command): + description = 'run test suite' + user_options = [] + + def initialize_options(self): + FORMAT = '%(asctime)-15s [%(levelname)s] %(message)s' + logging.basicConfig(format=FORMAT) + + THIS_DIR = os.path.abspath(os.path.split(__file__)[0]) + sys.path.insert(0, THIS_DIR) + for test_path in TEST_PATHS: + sys.path.insert(0, pjoin(THIS_DIR, test_path)) + self._dir = os.getcwd() + + def finalize_options(self): + pass + + def run(self): + self._run_mock_api_server() + status = self._run_tests() + sys.exit(status) + + def _run_tests(self): + testfiles = [] + for test_path in TEST_PATHS: + for t in glob(pjoin(self._dir, test_path, 'test_*.py')): + testfiles.append('.'.join( + [test_path.replace('/', '.'), splitext(basename(t))[0]])) + + tests = TestLoader().loadTestsFromNames(testfiles) + + t = TextTestRunner(verbosity=2) + res = t.run(tests) + return not res.wasSuccessful() + + def _run_mock_api_server(self): + for port in [8881, 8882, 8883]: + server = MockAPIServerRunner(port=port) + server.setUp() + + +setup(name='yubico', + version='.' . join(map(str, version)), + description='Python Yubico Client', + author='Tomaz Muraus', + author_email='tomaz+pypi@tomaz.me', + license='BSD', + url='http://github.com/Kami/python-yubico-client/', + download_url='http://github.com/Kami/python-yubico-client/downloads/', + packages=['yubico'], + provides=['yubico'], + install_requires=[ + 'requests == 1.1.0', + ], + cmdclass={ + 'test': TestCommand, + }, + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Security', + 'Topic :: Software Development :: Libraries :: Python Modules', + ] +) diff --git a/yubico-1.6.2.tar.gz/tests/__init__.py b/yubico-1.6.2.tar.gz/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yubico-1.6.2.tar.gz/tests/mock_http_server.py b/yubico-1.6.2.tar.gz/tests/mock_http_server.py new file mode 100755 index 0000000..42d69b5 --- /dev/null +++ b/yubico-1.6.2.tar.gz/tests/mock_http_server.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python + +import os +import time +import sys +import BaseHTTPServer + +from optparse import OptionParser +from os.path import join as pjoin +sys.path.append(pjoin(os.path.dirname(__file__), '../')) + +from yubico.yubico import BAD_STATUS_CODES + +mock_action = None +signature = None + + +class Handler(BaseHTTPServer.BaseHTTPRequestHandler): + + def do_GET(self): + global mock_action, signature + + if self.path.find('?') != -1: + self.path, self.query_string = self.path.split('?', 1) + split = self.query_string.split('&') + self.query_string = dict([pair.split('=', 1) for pair in split]) + + else: + self.query_string = {} + + if self.path == '/set_mock_action': + action = self.query_string['action'] + + if 'signature' in self.query_string: + signature = self.query_string['signature'] + else: + signature = None + + print 'Setting mock_action to %s' % (action) + mock_action = action + self._end(status_code=200) + return + + if mock_action in BAD_STATUS_CODES: + return self._send_status(status=mock_action) + elif mock_action == 'no_such_client': + return self._send_status(status='NO_SUCH_CLIENT') + elif mock_action == 'no_signature_ok': + return self._send_status(status='OK') + elif mock_action == 'ok_signature': + return self._send_status(status='OK', + signature=signature) + elif mock_action == 'no_signature_ok_invalid_otp_in_response': + return self._send_status(status='OK', + signature=signature, otp='different') + elif mock_action == 'no_signature_ok_invalid_nonce_in_response': + return self._send_status(status='OK', + signature=signature, nonce='different') + elif mock_action == 'timeout': + time.sleep(1) + return self._send_status(status='OK') + else: + self._end(status_code=500) + return + + def _end(self, status_code=200, body=''): + print 'Sending response: status_code=%s, body=%s' % (status_code, body) + self.send_response(status_code) + self.send_header('Content-Type', 'text/plain') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _send_status(self, status, signature=None, otp=None, nonce=None): + if signature: + body = '\nh=%s\nstatus=%s' % (signature, status) + else: + body = 'status=%s' % (status) + + if otp: + body += '&otp=%s' % (otp) + + if nonce: + body += '&nonce=%s' % (nonce) + + self._end(body=body) + + +def main(): + usage = 'usage: %prog --port=' + parser = OptionParser(usage=usage) + parser.add_option('--port', dest='port', default=8881, + help='Port to listen on', metavar='PORT') + + (options, args) = parser.parse_args() + + server_class = BaseHTTPServer.HTTPServer + httpd = server_class(('127.0.0.1', int(options.port)), Handler) + print 'Mock API server listening on 127.0.0.1:%s' % (options.port) + + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass + + httpd.server_close() + +main() diff --git a/yubico-1.6.2.tar.gz/tests/test_yubico.py b/yubico-1.6.2.tar.gz/tests/test_yubico.py new file mode 100644 index 0000000..bdf1d9b --- /dev/null +++ b/yubico-1.6.2.tar.gz/tests/test_yubico.py @@ -0,0 +1,152 @@ +import sys +import unittest + +import requests + +from yubico import yubico +from yubico.otp import OTP +from yubico.yubico_exceptions import StatusCodeError, InvalidClientIdError +from yubico.yubico_exceptions import SignatureVerificationError +from yubico.yubico_exceptions import InvalidValidationResponse + + +class TestOTPClass(unittest.TestCase): + def test_otp_class(self): + otp1 = OTP('tlerefhcvijlngibueiiuhkeibbcbecehvjiklltnbbl') + otp2 = OTP('jjjjjjjjnhe.ngcgjeiuujjjdtgihjuecyixinxunkhj', + translate_otp=True) + + self.assertEqual(otp1.device_id, 'tlerefhcvijl') + self.assertEqual(otp2.otp, + 'ccccccccljdeluiucdgffccchkugjcfditgbglbflvjc') + + def test_translation_multiple_interpretations(self): + otp_str1 = 'vvbtbtndhtlfguefgluvbdcetnitidgkvfkbicevgcin' + otp1 = OTP(otp_str1) + self.assertEqual(otp1.otp, otp_str1) + + def test_translation_single_interpretation(self): + otp_str1 = 'cccfgvgitchndibrrtuhdrgeufrdkrjfgutfjbnhhglv' + otp_str2 = 'cccagvgitchndibrrtuhdrgeufrdkrjfgutfjbnhhglv' + otp1 = OTP(otp_str1) + otp2 = OTP(otp_str2) + self.assertEqual(otp1.otp, otp_str1) + self.assertEqual(otp2.otp, otp_str2) + + +class TestYubicoVerifySingle(unittest.TestCase): + def setUp(self): + yubico.API_URLS = ('127.0.0.1:8881/wsapi/2.0/verify',) + yubico.DEFAULT_TIMEOUT = 2 + yubico.CA_CERTS_BUNDLE_PATH = None + + self.client_no_verify_sig = yubico.Yubico('1234', None, + use_https=False) + self.client_verify_sig = yubico.Yubico('1234', 'secret123456', + use_https=False) + + def test_invalid_custom_ca_certs_path(self): + if hasattr(sys, 'pypy_version_info'): + # TODO: Figure out why this breaks PyPy + return + + yubico.CA_CERTS_BUNDLE_PATH = '/does/not/exist.1' + client = yubico.Yubico('1234', 'secret123456') + + try: + client.verify('bad') + except requests.exceptions.SSLError: + pass + else: + self.fail('SSL exception was not thrown') + + def test_replayed_otp(self): + self._set_mock_action('REPLAYED_OTP') + + try: + self.client_no_verify_sig.verify('bad') + except StatusCodeError, e: + self.assertEqual(e.status_code, 'REPLAYED_OTP') + + def test_verify_bad_status_codes(self): + for status in (set(yubico.BAD_STATUS_CODES) - set(['REPLAYED_OTP'])): + self._set_mock_action(status) + + try: + self.client_no_verify_sig.verify('bad') + except Exception, e: + self.assertEqual(str(e), 'NO_VALID_ANSWERS') + + def test_verify_local_timeout(self): + self._set_mock_action('timeout') + + try: + self.client_no_verify_sig.verify('bad') + except Exception, e: + self.assertEqual(str(e), 'NO_VALID_ANSWERS') + + def test_verify_invalid_signature(self): + self._set_mock_action('no_signature_ok') + + try: + self.client_verify_sig.verify('test') + except SignatureVerificationError: + pass + else: + self.fail('Exception was not thrown') + + def test_verify_no_such_client(self): + self._set_mock_action('no_such_client') + + try: + self.client_no_verify_sig.verify('test') + except InvalidClientIdError, e: + self.assertEqual(e.client_id, '1234') + else: + self.fail('Exception was not thrown') + + def test_verify_ok_dont_check_signature(self): + self._set_mock_action('no_signature_ok') + + status = self.client_no_verify_sig.verify('test') + self.assertTrue(status) + + def test_verify_ok_check_signature(self): + signature = \ + self.client_verify_sig.generate_message_signature('status=OK') + self._set_mock_action('ok_signature', signature=signature) + + status = self.client_verify_sig.verify('test') + self.assertTrue(status) + + def test_verify_invalid_otp_returned_in_the_response(self): + self._set_mock_action('no_signature_ok_invalid_otp_in_response') + + try: + self.client_no_verify_sig.verify('test') + except InvalidValidationResponse, e: + self.assertTrue('Unexpected OTP in response' in e.message) + else: + self.fail('Exception was not thrown') + + def test_verify_invalid_nonce_returned_in_the_response(self): + self._set_mock_action('no_signature_ok_invalid_nonce_in_response') + + try: + self.client_no_verify_sig.verify('test') + except InvalidValidationResponse, e: + self.assertTrue('Unexpected nonce in response' in e.message) + else: + self.fail('Exception was not thrown') + + def _set_mock_action(self, action, port=8881, signature=None): + path = '/set_mock_action?action=%s' % (action) + + if signature: + path += '&signature=%s' % (signature) + + requests.get(url='http://127.0.0.1:%s%s' % (port, path)) + + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/yubico-1.6.2.tar.gz/tests/utils.py b/yubico-1.6.2.tar.gz/tests/utils.py new file mode 100644 index 0000000..7777e04 --- /dev/null +++ b/yubico-1.6.2.tar.gz/tests/utils.py @@ -0,0 +1,64 @@ +from __future__ import with_statement + +import os +import sys +import subprocess +import signal +import time +import socket +import errno +import atexit + +from os.path import join as pjoin + + +def waitForStartUp(process, address, timeout=10): + # connect to it, with a timeout in case something went wrong + start = time.time() + while time.time() < start + timeout: + try: + s = socket.create_connection(address) + s.close() + break + except: + time.sleep(0.1) + else: + # see if process is still alive + process.poll() + + if process and process.returncode is None: + process.terminate() + raise RuntimeError("Couldn't connect to server; aborting test") + + +class ProcessRunner(object): + def setUp(self, *args, **kwargs): + pass + + def tearDown(self, *args, **kwargs): + if self.process: + self.process.terminate() + + +class MockAPIServerRunner(ProcessRunner): + def __init__(self, port=8881): + self.port = port + + def setUp(self, *args, **kwargs): + self.cwd = os.getcwd() + self.process = None + self.base_dir = pjoin(self.cwd) + self.log_path = pjoin(self.cwd, 'mock_api_server.log') + + super(MockAPIServerRunner, self).setUp(*args, **kwargs) + script = pjoin(os.path.dirname(__file__), 'mock_http_server.py') + + with open(self.log_path, 'a+') as log_fp: + args = '%s --port=%s' % (script, str(self.port)) + args = [script, '--port=%s' % (self.port)] + + self.process = subprocess.Popen(args, shell=False, + cwd=self.base_dir, stdout=log_fp, + stderr=log_fp) + waitForStartUp(self.process, ('127.0.0.1', self.port), 10) + atexit.register(self.tearDown) diff --git a/yubico-1.6.2.tar.gz/yubico.egg-info/PKG-INFO b/yubico-1.6.2.tar.gz/yubico.egg-info/PKG-INFO new file mode 100644 index 0000000..8ec90e4 --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico.egg-info/PKG-INFO @@ -0,0 +1,21 @@ +Metadata-Version: 1.1 +Name: yubico +Version: 1.6.2 +Summary: Python Yubico Client +Home-page: http://github.com/Kami/python-yubico-client/ +Author: Tomaz Muraus +Author-email: tomaz+pypi@tomaz.me +License: BSD +Download-URL: http://github.com/Kami/python-yubico-client/downloads/ +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Security +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Provides: yubico diff --git a/yubico-1.6.2.tar.gz/yubico.egg-info/SOURCES.txt b/yubico-1.6.2.tar.gz/yubico.egg-info/SOURCES.txt new file mode 100644 index 0000000..af982fc --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico.egg-info/SOURCES.txt @@ -0,0 +1,19 @@ +LICENSE +MANIFEST.in +NOTICE +README.md +setup.py +tests/__init__.py +tests/mock_http_server.py +tests/test_yubico.py +tests/utils.py +yubico/__init__.py +yubico/modhex.py +yubico/otp.py +yubico/yubico.py +yubico/yubico_exceptions.py +yubico.egg-info/PKG-INFO +yubico.egg-info/SOURCES.txt +yubico.egg-info/dependency_links.txt +yubico.egg-info/requires.txt +yubico.egg-info/top_level.txt \ No newline at end of file diff --git a/yubico-1.6.2.tar.gz/yubico.egg-info/dependency_links.txt b/yubico-1.6.2.tar.gz/yubico.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/yubico-1.6.2.tar.gz/yubico.egg-info/requires.txt b/yubico-1.6.2.tar.gz/yubico.egg-info/requires.txt new file mode 100644 index 0000000..fefbfd2 --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico.egg-info/requires.txt @@ -0,0 +1 @@ +requests == 1.1.0 \ No newline at end of file diff --git a/yubico-1.6.2.tar.gz/yubico.egg-info/top_level.txt b/yubico-1.6.2.tar.gz/yubico.egg-info/top_level.txt new file mode 100644 index 0000000..0ad4a6b --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico.egg-info/top_level.txt @@ -0,0 +1 @@ +yubico diff --git a/yubico-1.6.2.tar.gz/yubico/__init__.py b/yubico-1.6.2.tar.gz/yubico/__init__.py new file mode 100644 index 0000000..5e14efa --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico/__init__.py @@ -0,0 +1 @@ +__version__ = (1, 6, 2) diff --git a/yubico-1.6.2.tar.gz/yubico/modhex.py b/yubico-1.6.2.tar.gz/yubico/modhex.py new file mode 100644 index 0000000..3ac5507 --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico/modhex.py @@ -0,0 +1,149 @@ +# -*- encoding: utf-8 -*- +# +# Copyright (c) 2009 Daniel Holth +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +__all__ = ["HEX", "MODHEX", "translate"] + +# Possible Yubikey alphabets. Generated by code at +# http://bitbucket.org/dholth/yubikey/ +alphabets = u""",yuéebfcstnxg.vi +cbdefghijklnrtuv +cbdefghijklnrtuṣ +cbdefghıjklnrtuv +cbdefghıíklnrtuv +cbdefghıĭklnrtuv +cbdešghijklnrtuv +cbsftdhuneikpglv +cbɗefghijklnrtuv +cžsrtmpuneišldjv +gwcbdthoeaznyusv +jka.oumtdsrqhxlb +jka.oumtdsrĝhĉlb +jxe.iuhcdrnbpygk +jxe.uidchtnbpygk +jxeñuidcrtnbpygk +jxeöuidchtnbpygk +vçexautnkmlziorc +vçeğaütnkmlzıorc +xizqaehutdcn.os, +xkipe,cdtsr'oèv. +¯¦É¿¸À̀¨¾Á¼¶ºÅñ +äzaleosgnrtbcwhp +çbdefghijklnrtuý +čñďéëŕúíüôľňřťůç +ŋbdefghijklnrtuv +ŋbdefghiƒklnrtuv +ψβδεφγηιξκλνρτθω +жбдефгңийклнртув +йще.уидцхтнбпыгк +сивуапршолдткегм +сиқуапршолдткегм +сівуапршолдткегм +цбдефгхийклнртув +цбдефгхийклнртуж +цбдефгхијклнртув +цбдефгхійклнртуж +цбдефгчийклнртуж +цбдефґгійклнртув +ъфаеожгстнвхишкэ +ёмбуөахшролижэгс +գբդէֆքհիճկլնրտըվ +գպտէֆկհիճքլնրդըւ +չզգբեանկիտհլսմւյ +ցբդեֆգհիյկլնռտւվ +ցբդեֆգհիյկլնրտւվ +քբդէֆգհիճկլնրտըվ +בנגקכעיןחלךמראוה +ؤﻻيثبلاهتنمىقفعر +جبدەفگهحژکلنرتئڤ +زذیثبلاهتنمدقفعر +چبدعفگحیجکلنرتءط +چبدعفگھیجکلنرتءط +ےشرھنلہباکیغدٹتس +ܤܒܕܖܔܓܗܥܛܟܠܢܪܬܜܫ +ܤܧܝܖܒܠܐܗܬܢܡ܀ܩܦܥܪ +ޗބދެފގހިޖކލނރތުވ +ߗߓߘߍߝߜߤߌߖߞߟߣߙߕߎߢ +चबडेङगहिजकलनरटुव +चबदेटगहिजकलनरतुड +छबदेउगहिजकलनरतुव +मव्ािुपगरकतलीूहन +চবডীতগহিজকলনরটুআ +মব্ািুপগরকতলীূহন +েনিডব্াহকতদসপটজর +ਚਬ੍ਾਿੁਹਗਜਕਲਨੀੂਦਵ +ਮਵ੍ਾਿੁਪਗਰਕਤਲੀੂਹਨ +મવ્ાિુપગરકતલીૂહન +ମଵ୍ାିୁପଗରକତଲୀୂହନ +உெனநகபாைதமடஔசவரஎ +మవ్ాిుపగరకతలీూహన +ಮವ್ಾಿುಪಗರಕತಲೀೂಹನ +ചബദെഫഗഹിജകലനരതുവ +മവ്ാിുപഗരകതലീൂഹന +චබදඑෆගහඉජකලනරතඋව +ลิงยกัีมานเคอรดห +แิกำดเ้ร่าสืพะีอ +ແຶກຳດເ້ຣ່າສືພະີອ +ཀཔདེབངམི་གལནརཏུཁ +འརདགནཔཕོབམཙལངིེཡ +မဗ္ာိုပဂရကတလီူဟန +სივუაპრშოლდტკეგმ +ყჟაუეოდნმსრზძჭთღ +ცბდეფგჰიჯკლნრტუვ +ቸበደeፈገሀiጀከለነረተuሸ +ᏓᎨᏗᎡᎩᎦᎯᎢᏚᎸᎵᎾᏛᏔᎤᎥ +ᖃᑕᖁᕿᑯᑐᓱᓂᒧᓄᓗᓴᑭᑎᒥᑲ +ᚉᚁᚇᚓᚃᚌᚆᚔᚗᚖᚂᚅᚏᚈᚒᚍ +ចបដេថងហិ្កលនរតុវ +ⵛⴱⴷⴻⴼⴳⵀⵉⵊⴽⵍⵏⵔⵜⵓⵖ +ソコシイハキクニマノリミスカナヒ""".split(u"\n") + +index = {} +for i, alphabet in enumerate(alphabets): + for letter in alphabet: + index.setdefault(letter, set()).update([i]) + +HEX = u"0123456789abcdef" +MODHEX = u"cbdefghijklnrtuv" + + +def translate(otp, to=MODHEX): + """Return set() of possible modhex interpretations of a Yubikey otp. + + If otp uses all 16 characters in its alphabet, there will be only + one possible interpretation of that Yubikey otp (except for two + Armenian keyboard layouts). + + otp: Yubikey output. + to: 16-character target alphabet, default MODHEX. + """ + if not isinstance(otp, unicode): + raise ValueError("otp must be unicode") + if not isinstance(to, unicode): + raise ValueError("to must be unicode") + possible = (set(index[c]) for c in set(otp)) + possible = reduce(lambda a, b: a.intersection(b), possible) + translated = set() + for i in possible: + a = alphabets[i] + translation = dict(zip((ord(c) for c in a), to)) + translated.add(otp.translate(translation)) + return translated diff --git a/yubico-1.6.2.tar.gz/yubico/otp.py b/yubico-1.6.2.tar.gz/yubico/otp.py new file mode 100644 index 0000000..a1ff26d --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico/otp.py @@ -0,0 +1,39 @@ +""" +Class which holds data about an OTP. +""" + +import modhex + + +class OTP(object): + def __init__(self, otp, translate_otp=True): + self.otp = self.get_otp_modehex_interpretation(otp) \ + if translate_otp else otp + + self.device_id = self.otp[:12] + self.session_counter = None + self.timestamp = None + self.session_user = None + + def get_otp_modehex_interpretation(self, otp): + # We only use the first interpretation, because + # if the OTP uses all 16 characters in its alphabet + # there is only one possible interpretation of that otp + try: + interpretations = modhex.translate(unicode(otp)) + except Exception: + return otp + + if len(interpretations) == 0: + return otp + elif len(interpretations) > 1: + # If there are multiple interpretations first try to use the same + # translation as the input OTP. If the one is not found, use the + # random interpretation. + if unicode(otp) in interpretations: + return otp + + return interpretations.pop() + + def __repr__(self): + return '%s, %s, %s' % (self.otp, self.device_id, self.timestamp) diff --git a/yubico-1.6.2.tar.gz/yubico/yubico.py b/yubico-1.6.2.tar.gz/yubico/yubico.py new file mode 100644 index 0000000..4c332bf --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico/yubico.py @@ -0,0 +1,374 @@ +# -*- coding: utf-8 -*- +# +# Name: Yubico Python Client +# Description: Python class for verifying Yubico One Time Passwords (OTPs). +# +# Author: Tomaž Muraus (http://www.tomaz.me) +# License: BSD +# +# Copyright (c) 2010, Tomaž Muraus +# Copyright (c) 2012, Yubico AB +# All rights reserved. +# +# Requirements: +# - Python >= 2.5 + +import re +import os +import time +import urllib +import hmac +import base64 +import hashlib +import threading +import logging + +import requests + +from otp import OTP +from yubico_exceptions import (StatusCodeError, InvalidClientIdError, + InvalidValidationResponse, + SignatureVerificationError) + +logger = logging.getLogger('yubico.client') + +# Path to the custom CA certificates bundle. Only used if set. +CA_CERTS_BUNDLE_PATH = None + +COMMON_CA_LOCATIONS = [ + '/usr/local/lib/ssl/certs/ca-certificates.crt', + '/usr/local/ssl/certs/ca-certificates.crt', + '/usr/local/share/curl/curl-ca-bundle.crt', + '/usr/local/etc/openssl/cert.pem', + '/opt/local/lib/ssl/certs/ca-certificates.crt', + '/opt/local/ssl/certs/ca-certificates.crt', + '/opt/local/share/curl/curl-ca-bundle.crt', + '/opt/local/etc/openssl/cert.pem', + '/usr/lib/ssl/certs/ca-certificates.crt', + '/usr/ssl/certs/ca-certificates.crt', + '/usr/share/curl/curl-ca-bundle.crt', + '/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/cert.pem', + '/etc/pki/CA/cacert.pem', + 'C:\Windows\curl-ca-bundle.crt', + 'C:\Windows\ca-bundle.crt', + 'C:\Windows\cacert.pem' +] + +API_URLS = ('api.yubico.com/wsapi/2.0/verify', + 'api2.yubico.com/wsapi/2.0/verify', + 'api3.yubico.com/wsapi/2.0/verify', + 'api4.yubico.com/wsapi/2.0/verify', + 'api5.yubico.com/wsapi/2.0/verify') + +DEFAULT_TIMEOUT = 10 # How long to wait before the time out occurs +DEFAULT_MAX_TIME_WINDOW = 40 # How many seconds can pass between the first + # and last OTP generations so the OTP is + # still considered valid (only used in the + # multi mode) default is 5 seconds + # (40 / 0.125 = 5) + +BAD_STATUS_CODES = ['BAD_OTP', 'REPLAYED_OTP', 'BAD_SIGNATURE', + 'MISSING_PARAMETER', 'OPERATION_NOT_ALLOWED', + 'BACKEND_ERROR', 'NOT_ENOUGH_ANSWERS', + 'REPLAYED_REQUEST'] + + +class Yubico(object): + def __init__(self, client_id, key=None, use_https=True, verify_cert=True, + translate_otp=True): + self.client_id = client_id + self.key = base64.b64decode(key) if key is not None else None + self.use_https = use_https + self.verify_cert = verify_cert + self.translate_otp = translate_otp + + def verify(self, otp, timestamp=False, sl=None, timeout=None, + return_response=False): + """ + Returns True is the provided OTP is valid, + False if the REPLAYED_OTP status value is returned or the response + message signature verification failed and None for the rest of the + status values. + """ + ca_bundle_path = self._get_ca_bundle_path() + + otp = OTP(otp, self.translate_otp) + nonce = base64.b64encode(os.urandom(30), 'xz')[:25] + query_string = self.generate_query_string(otp.otp, nonce, timestamp, + sl, timeout) + request_urls = self.generate_request_urls() + + threads = [] + timeout = timeout or DEFAULT_TIMEOUT + for url in request_urls: + thread = URLThread('%s?%s' % (url, query_string), timeout, + self.verify_cert, ca_bundle_path) + thread.start() + threads.append(thread) + + # Wait for a first positive or negative response + start_time = time.time() + while threads and (start_time + timeout) > time.time(): + for thread in threads: + if not thread.is_alive(): + if thread.exception: + raise thread.exception + elif thread.response: + status = self.verify_response(thread.response, + otp.otp, nonce, + return_response) + + if status: + if return_response: + return status + else: + return True + threads.remove(thread) + time.sleep(0.1) + + # Timeout or no valid response received + raise Exception('NO_VALID_ANSWERS') + + def verify_multi(self, otp_list=None, max_time_window=None, sl=None, + timeout=None): + # Create the OTP objects + otps = [] + for otp in otp_list: + otps.append(OTP(otp, self.translate_otp)) + + device_ids = set() + for otp in otps: + device_ids.add(otp.device_id) + + # Check that all the OTPs contain same device id + if len(device_ids) != 1: + raise Exception('OTPs contain different device ids') + + # Now we verify the OTPs and save the server response for each OTP. + # We need the server response, to retrieve the timestamp. + # It's possible to retrieve this value locally, without querying the + # server but in this case, user would need to provide his AES key. + for otp in otps: + response = self.verify(otp.otp, True, sl, timeout, + return_response=True) + + if not response: + return False + + otp.timestamp = int(response['timestamp']) + + count = len(otps) + delta = otps[count - 1].timestamp - otps[0].timestamp + + if max_time_window: + max_time_window = (max_time_window / 0.125) + else: + max_time_window = DEFAULT_MAX_TIME_WINDOW + + if delta > max_time_window: + raise Exception('More then %s seconds has passed between ' + + 'generating the first and the last OTP.' % + (max_time_window * 0.125)) + + return True + + def verify_response(self, response, otp, nonce, return_response=False): + """ + Returns True if the OTP is valid (status=OK) and return_response=False, + otherwise (return_response = True) it returns the server response as a + dictionary. + + Throws an exception if the OTP is replayed, the server response message + verification failed or the client id is invalid, returns False + otherwise. + """ + try: + status = re.search(r'status=([A-Z0-9_]+)', response) \ + .groups() + + if len(status) > 1: + message = 'More than one status= returned. Possible attack!' + raise InvalidValidationResponse(message, response) + + status = status[0] + except (AttributeError, IndexError): + return False + + signature, parameters = \ + self.parse_parameters_from_response(response) + + # Secret key is specified, so we verify the response message + # signature + if self.key: + generated_signature = \ + self.generate_message_signature(parameters) + + # Signature located in the response does not match the one we + # have generated + if signature != generated_signature: + raise SignatureVerificationError(generated_signature, + signature) + param_dict = self.get_parameters_as_dictionary(parameters) + + if 'otp' in param_dict and param_dict['otp'] != otp: + message = 'Unexpected OTP in response. Possible attack!' + raise InvalidValidationResponse(message, response, param_dict) + + if 'nonce' in param_dict and param_dict['nonce'] != nonce: + message = 'Unexpected nonce in response. Possible attack!' + raise InvalidValidationResponse(message, response, param_dict) + + if status == 'OK': + if return_response: + return param_dict + else: + return True + elif status == 'NO_SUCH_CLIENT': + raise InvalidClientIdError(self.client_id) + elif status == 'REPLAYED_OTP': + raise StatusCodeError(status) + + return False + + def generate_query_string(self, otp, nonce, timestamp=False, sl=None, + timeout=None): + """ + Returns a query string which is sent to the validation servers. + """ + data = [('id', self.client_id), + ('otp', otp), + ('nonce', nonce)] + + if timestamp: + data.append(('timestamp', '1')) + + if sl is not None: + if sl not in range(0, 101) and sl not in ['fast', 'secure']: + raise Exception('sl parameter value must be between 0 and ' + '100 or string "fast" or "secure"') + + data.append(('sl', sl)) + + if timeout: + data.append(('timeout', timeout)) + + query_string = urllib.urlencode(data) + + if self.key: + hmac_signature = self.generate_message_signature(query_string) + query_string += '&h=%s' % (hmac_signature.replace('+', '%2B')) + + return query_string + + def generate_message_signature(self, query_string): + """ + Returns a HMAC-SHA-1 signature for the given query string. + http://goo.gl/R4O0E + """ + pairs = query_string.split('&') + pairs = [pair.split('=') for pair in pairs] + pairs_sorted = sorted(pairs) + pairs_string = '&' . join(['=' . join(pair) for pair in pairs_sorted]) + + digest = hmac.new(self.key, pairs_string, hashlib.sha1).digest() + signature = base64.b64encode(digest) + + return signature + + def parse_parameters_from_response(self, response): + """ + Returns a response signature and query string generated from the + server response. 'h' aka signature argument is stripped from the + returned query string. + """ + split = [pair.strip() for pair in response.split('\n') + if pair.strip() != ''] + query_string = '&' . join(split) + split_dict = self.get_parameters_as_dictionary(query_string) + + if 'h' in split_dict: + signature = split_dict['h'] + del split_dict['h'] + else: + signature = None + + query_string = '' + for index, (key, value) in enumerate(split_dict.iteritems()): + query_string += '%s=%s' % (key, value) + + if index != len(split_dict) - 1: + query_string += '&' + + return (signature, query_string) + + def get_parameters_as_dictionary(self, query_string): + """ Returns query string parameters as a dictionary. """ + dictionary = dict([parameter.split('=', 1) for parameter + in query_string.split('&')]) + + return dictionary + + def generate_request_urls(self): + """ + Returns a list of the API URLs. + """ + urls = [] + for url in API_URLS: + if self.use_https: + url = 'https://%s' % (url) + else: + url = 'http://%s' % (url) + urls.append(url) + + return urls + + def _get_ca_bundle_path(self): + """ + Return a path to the CA bundle which is used for verifying the hosts + SSL certificate. + """ + if CA_CERTS_BUNDLE_PATH: + # User provided a custom path + return CA_CERTS_BUNDLE_PATH + + for file_path in COMMON_CA_LOCATIONS: + if os.path.exists(file_path) and os.path.isfile(file_path): + return file_path + + return None + + +class URLThread(threading.Thread): + def __init__(self, url, timeout, verify_cert, ca_bundle_path=None): + super(URLThread, self).__init__() + self.url = url + self.timeout = timeout + self.verify_cert = verify_cert + self.ca_bundle_path = ca_bundle_path + self.exception = None + self.request = None + self.response = None + + def run(self): + logger.debug('Sending HTTP request to %s (thread=%s)' % (self.url, + self.name)) + verify = self.verify_cert + + if self.ca_bundle_path is not None: + verify = self.ca_bundle_path + logger.debug('Using custom CA bunde: %s' % (self.ca_bundle_path)) + + try: + self.request = requests.get(url=self.url, timeout=self.timeout, + verify=verify) + self.response = self.request.content + except requests.exceptions.SSLError, e: + self.exception = e + self.response = None + except Exception, e: + logger.error('Failed to retrieve response: ' + str(e)) + self.response = None + + args = (self.url, self.name, self.response) + logger.debug('Received response from %s (thread=%s): %s' % args) diff --git a/yubico-1.6.2.tar.gz/yubico/yubico_exceptions.py b/yubico-1.6.2.tar.gz/yubico/yubico_exceptions.py new file mode 100644 index 0000000..891336c --- /dev/null +++ b/yubico-1.6.2.tar.gz/yubico/yubico_exceptions.py @@ -0,0 +1,51 @@ +__all___ = [ + 'YubicoError', + 'StatusCodeError', + 'InvalidClientIdError', + 'InvalidValidationResponse', + 'SignatureVerificationError' +] + + +class YubicoError(Exception): + """ Base class for Yubico related exceptions. """ + pass + + +class StatusCodeError(YubicoError): + def __init__(self, status_code): + self.status_code = status_code + + def __str__(self): + return ('Yubico server returned the following status code: %s' % + (self.status_code)) + + +class InvalidClientIdError(YubicoError): + def __init__(self, client_id): + self.client_id = client_id + + def __str__(self): + return 'The client with ID %s does not exist' % (self.client_id) + + +class InvalidValidationResponse(YubicoError): + def __init__(self, reason, response, parameters=None): + self.reason = reason + self.response = response + self.parameters = parameters + self.message = self.reason + + def __str__(self): + return self.reason + + +class SignatureVerificationError(YubicoError): + def __init__(self, generated_signature, response_signature): + self.generated_signature = generated_signature + self.response_signature = response_signature + + def __str__(self): + return repr('Server response message signature verification failed' + + '(expected %s, got %s)' % (self.generated_signature, + self.response_signature)) -- Gitee