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 Binary files a/python-yubico-1.3.3.tar.gz and /dev/null differ diff --git a/python-yubico.spec b/python-yubico.spec index ae0d02e234b4e1068a562f2e33da95923e61d95a..6bafe53f9ff7d4b15fc2038d3dfa4cd794ad4d9e 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 0000000000000000000000000000000000000000..c034025e6dd4eacc8e963e3f14239832ec5bd86c --- /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 0000000000000000000000000000000000000000..3925e5d42f87e8a76c66aea39fe22794788d0246 --- /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 0000000000000000000000000000000000000000..2317236a25c601159f005bfb1c2fd42dc03b0d2e --- /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 0000000000000000000000000000000000000000..8ec90e4f090a0c411276febc398ee1f4d394986c --- /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 0000000000000000000000000000000000000000..5433b9bf544df09cd72a6f9a2e3e7348fe10a903 --- /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 0000000000000000000000000000000000000000..861a9f554263efb088d8636c4f17a30696e495ad --- /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 0000000000000000000000000000000000000000..b5c8492dabd10ee4f7bcdd9e01109540f2682190 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 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 0000000000000000000000000000000000000000..42d69b52f5220574fb3643de0aea59b9153369d7 --- /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 0000000000000000000000000000000000000000..bdf1d9b8818b77dd0d826c2733b2d4f63271c2fa --- /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 0000000000000000000000000000000000000000..7777e042332a90d7c5f808585588a6c6c9000e67 --- /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 0000000000000000000000000000000000000000..8ec90e4f090a0c411276febc398ee1f4d394986c --- /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 0000000000000000000000000000000000000000..af982fcfd6b536d0c778fb1ff42bcd32c8ddaa7b --- /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 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /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 0000000000000000000000000000000000000000..fefbfd229f320b366ed0b8d838a51c5cca9c2e33 --- /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 0000000000000000000000000000000000000000..0ad4a6b1ec7bb824956165a7d029d559a23a9fea --- /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 0000000000000000000000000000000000000000..5e14efa8df944ef3733ee2e2c3d4b8640eb1a28d --- /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 0000000000000000000000000000000000000000..3ac55073a3acc7a948ec27ecf79607938fb529b5 --- /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 0000000000000000000000000000000000000000..a1ff26de14a7c5fbb9602e44584e4fd21efbd429 --- /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 0000000000000000000000000000000000000000..4c332bfb149e8a7ef2720484c03adf912915ab63 --- /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 0000000000000000000000000000000000000000..891336c8ba0c4d4c6bda534f3bc9129b0303a224 --- /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))