From 868edec1679eb6f101dc99742f4bf8c8d7e09786 Mon Sep 17 00:00:00 2001 From: Felipe Date: Mon, 8 Jul 2013 10:24:05 -0400 Subject: [PATCH 1/6] Ignore .pyc files --- .gitignore | 4 +++- yubikey.pyc | Bin 7263 -> 0 bytes 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 yubikey.pyc diff --git a/.gitignore b/.gitignore index 98d45c9..9a8364e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.c9revisions/ \ No newline at end of file +.c9revisions/ +tests.py +*.pyc diff --git a/yubikey.pyc b/yubikey.pyc deleted file mode 100644 index ce6b2dcaa86a9ec3b1e7ccc46eb7a09f4afb2e4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7263 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 Date: Mon, 8 Jul 2013 10:29:07 -0400 Subject: [PATCH 2/6] Updated README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 474bdb2..621920b 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,17 @@ pip install python-yubikey from yubikey import Yubikey yubi = Yubikey() -yubi.register('') +yubi.register('', '') # yubi.id and yubi.key are now set ``` ## Check valid OTP ``` +yubi = Yubikey(, ) result = yubi.verify('') # True / False +# If is provided, requests will be signed and the responses checked. ``` # NO WARRANTY From c8b078d0c11ccca836ba49c1c7599da2825a77a4 Mon Sep 17 00:00:00 2001 From: Felipe Date: Mon, 8 Jul 2013 10:29:26 -0400 Subject: [PATCH 3/6] Removed tests file --- tests.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 tests.py diff --git a/tests.py b/tests.py deleted file mode 100644 index d59be43..0000000 --- a/tests.py +++ /dev/null @@ -1,5 +0,0 @@ -from yubikey import Yubikey - - -yubi = Yubikey(123) -yubi.verify('asdadadasdasdadadadadasdasdadsasdadadsadadad') \ No newline at end of file From 9bcae287e4b411a02b6a7c90dba098d249fd4337 Mon Sep 17 00:00:00 2001 From: Felipe Date: Mon, 8 Jul 2013 11:28:17 -0400 Subject: [PATCH 4/6] Added hmac-sha1 signature --- yubikey.py | 148 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 50 deletions(-) diff --git a/yubikey.py b/yubikey.py index 7efa2f9..3835404 100644 --- a/yubikey.py +++ b/yubikey.py @@ -1,15 +1,18 @@ #!/usr/bin/env python - -import requests import string from random import choice +import base64 +from hashlib import sha1 +import hmac +import requests class YubicoWS(object): register_ws = 'https://upgrade.yubico.com/getapikey/?format=json' - _api_ws = 'http://%s/wsapi/2.0/' api_ws = None - + + _protocol = 'https' + _servers = [ 'api.yubico.com', 'api2.yubico.com', @@ -17,25 +20,37 @@ class YubicoWS(object): 'api4.yubico.com', 'api5.yubico.com', ] - + _server = None + + _api_ws = '%s://%s/wsapi/2.0/' + _errors = { 'BAD_OTP': 'The OTP is invalid format.', 'REPLAYED_OTP': 'The OTP has already been seen by the service.', 'BAD_SIGNATURE': 'The HMAC signature verification failed.', 'MISSING_PARAMETER': 'The request lacks a parameter.', 'NO_SUCH_CLIENT': 'The request id does not exist.', - 'OPERATION_NOT_ALLOWED': 'The request id is not allowed to verify OTPs.', - 'BACKEND_ERROR': 'Unexpected error in our server. Please contact us if you see this error.', - 'NOT_ENOUGH_ANSWERS': 'Server could not get requested number of syncs during before timeout', + 'OPERATION_NOT_ALLOWED': 'The request id is not allowed ' + 'to verify OTPs.', + 'BACKEND_ERROR': 'Unexpected error in our server. Please contact ' + 'us if you see this error.', + # 2.0 + 'NOT_ENOUGH_ANSWERS': 'Server could not get requested number of syncs ' + 'during before timeout', 'REPLAYED_REQUEST': 'Server has seen the OTP/Nonce combination before', } - - def __init__(self): - self.select_random_server() - def select_random_server(self): - "Select random API Server" - self.api_ws = self._api_ws % choice(self._servers) + def __init__(self, **kwargs): + self._protocol = kwargs.get('protocol', self._protocol) + self._server = kwargs.get('server', None) + self.select_server() + + def select_server(self): + "Select server if provided, otherwise uses one from the list" + if self._server and self._server in self._servers: + self.api_ws = self._api_ws % (self._protocol, self._server) + else: + self.api_ws = self._api_ws % (self._protocol, choice(self._servers)) def register_api_key(self, email, otp): "Registers an API Key with the servers" @@ -46,57 +61,84 @@ class YubicoWS(object): response = requests.post(self.register_ws, data) ws_response = response.json() if not ws_response['status']: - raise WSError(ws_response['error']) - + raise YubicoWSError(ws_response['error']) + return ws_response - + def verify(self, yubikey_id, otp, key=None): "Verifies the provided OTP with the server" endpoint = 'verify' url = self.api_ws + endpoint - + # Check otp format if not (len(otp) > 32 and len(otp) < 48): raise OTPIncorrectFormat() - + nonce = self.generate_nonce() - + data = { - 'id': int(yubikey_id), + 'id': str(yubikey_id), 'otp': str.lower(otp), 'nonce': nonce } - + # Use API key for signing the message if key is provided if key: - data = self.sign_otp(data, key) - + data['h'] = self.sign(data, key).replace('+', '%2B') + response = requests.get(url, params=data) - + ws_response = self.parse_ws_response(response.text) - print(ws_response) + if ws_response['status'] == 'OK': # Check if response is valid if not (ws_response['nonce'] == nonce \ and ws_response['otp'] != otp \ and True): - raise WSInvalidResponse() - # TODO check signature + raise YubicoWSInvalidResponse() + + if key: + signature = self.sign(ws_response, key) + + if data['h'] != signature: + raise YubicoWSResponseBadSignature( + "The signature sent by the server is invalid" + ) + else: - raise WSError(self._errors[ws_response['status']]) - + raise YubicoWSError(self._errors[ws_response['status']]) + return ws_response - def sign_otp(self, data, key): - "Signs the OTP with the provided key" - return data + def sign(self, data, key): + "Signs the message with the provided key" + if 'h' in data: + # Just in case + data.pop('h') + + # Sorted k=v dict + params = [] + for k in sorted(data.keys()): + key_value = "%s=%s" % (k, data[k]) + params.append(key_value) + + # Join as urlparams + string = '&'.join(params) + + # hmac-sha1 + hashed_string = hmac.new(base64.b64decode(key), string, sha1).digest() + + # base64 encode + signature = base64.b64encode(hashed_string) + + return signature def parse_ws_response(self, text): "Parses the API key=value response into a dict" data = {} - for line in text.strip().split('\n'): + for line in text.split(): key, value = line.split('=', 1) - data[key] = value + data[key.strip()] = value.strip() return data def generate_nonce(self): @@ -109,11 +151,11 @@ class Yubikey(object): id = None key = None prefix = None - + _last_result = False - - def __init__(self, yubikey_id=None, key=None): - self.ws = YubicoWS() + + def __init__(self, yubikey_id=None, key=None, **kwargs): + self.ws = YubicoWS(**kwargs) if yubikey_id: self.id = yubikey_id if key: @@ -128,36 +170,38 @@ class Yubikey(object): self.id = credentials['id'] self.key = credentials['key'] result = True - + return result - + def verify(self, otp): "Verify an OTP to check if its valid" result = False if self.id: self.get_prefix(otp) - result = self.ws.verify(self.id, otp, key=self.key) - if result == 'OK': + try: + response = self.ws.verify(self.id, otp, key=self.key) result = True - + except YubicoWSResponseBadSignature, YubicoWSError: + result = False + self._last_result = result return result - + def get_prefix(self, otp): "Get prefix from an OTP if present" if len(otp) > 32: self.prefix = str.lower(otp[:-32]) - -class WSError(Exception): + +class YubicoWSError(Exception): def __init__(self, message=None): self.msg = "Web Service responded with an error: %s" % message - + def __str__(self): return repr(self.msg) - -class WSInvalidResponse(Exception): + +class YubicoWSInvalidResponse(Exception): msg = 'Response from the server is invalid' @@ -168,3 +212,7 @@ class WSResponseError(Exception): class OTPIncorrectFormat(Exception): pass + + +class YubicoWSResponseBadSignature(Exception): + pass From 7ec3fad7f115dcc69f94090cde42fd7d474c387c Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 8 Jul 2013 22:34:33 +0200 Subject: [PATCH 5/6] Fixed all PEP errors --- yubikey.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/yubikey.py b/yubikey.py index 3835404..c822ed0 100644 --- a/yubikey.py +++ b/yubikey.py @@ -50,7 +50,10 @@ class YubicoWS(object): if self._server and self._server in self._servers: self.api_ws = self._api_ws % (self._protocol, self._server) else: - self.api_ws = self._api_ws % (self._protocol, choice(self._servers)) + self.api_ws = self._api_ws % ( + self._protocol, + choice(self._servers) + ) def register_api_key(self, email, otp): "Registers an API Key with the servers" @@ -92,9 +95,9 @@ class YubicoWS(object): if ws_response['status'] == 'OK': # Check if response is valid - if not (ws_response['nonce'] == nonce \ - and ws_response['otp'] != otp \ - and True): + if not (ws_response['nonce'] == nonce + and ws_response['otp'] != otp + and True): raise YubicoWSInvalidResponse() if key: @@ -123,10 +126,14 @@ class YubicoWS(object): params.append(key_value) # Join as urlparams - string = '&'.join(params) + parameters = '&'.join(params) # hmac-sha1 - hashed_string = hmac.new(base64.b64decode(key), string, sha1).digest() + hashed_string = hmac.new( + base64.b64decode(key), + parameters, + sha1 + ).digest() # base64 encode signature = base64.b64encode(hashed_string) @@ -179,9 +186,9 @@ class Yubikey(object): if self.id: self.get_prefix(otp) try: - response = self.ws.verify(self.id, otp, key=self.key) + self.ws.verify(self.id, otp, key=self.key) result = True - except YubicoWSResponseBadSignature, YubicoWSError: + except (YubicoWSResponseBadSignature, YubicoWSError): result = False self._last_result = result From a66d862d044d56dd4c7c4cc693d879911c6facb8 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 8 Jul 2013 22:54:07 +0200 Subject: [PATCH 6/6] Signature fully working, updated readme --- README.md | 13 ++++++++++++- yubikey.py | 21 +++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 621920b..c9666bd 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,18 @@ result = yubi.verify('') # If is provided, requests will be signed and the responses checked. ``` +## Optionals + +``` +# Using custom API server +# Must be one of YubicoWS._servers +yubi = Yubikey(123, 'dGhpc3JlcG9yb2Nrcw==', server='api2.yubico.com') + +# Using http instead of https +yubi = Yubikey(123, 'dGhpc3JlcG9yb2Nrcw==', protocol='http') +``` + # NO WARRANTY THE PROGRAM IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY WARRANTY. IT IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW THE AUTHOR WILL BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. \ No newline at end of file +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW THE AUTHOR WILL BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. diff --git a/yubikey.py b/yubikey.py index c822ed0..b81d590 100644 --- a/yubikey.py +++ b/yubikey.py @@ -87,7 +87,7 @@ class YubicoWS(object): # Use API key for signing the message if key is provided if key: - data['h'] = self.sign(data, key).replace('+', '%2B') + data['h'] = self.sign(data, key) response = requests.get(url, params=data) @@ -95,15 +95,14 @@ class YubicoWS(object): if ws_response['status'] == 'OK': # Check if response is valid - if not (ws_response['nonce'] == nonce - and ws_response['otp'] != otp - and True): + if not (ws_response['nonce'] == data['nonce'] + and ws_response['otp'] == otp): raise YubicoWSInvalidResponse() if key: signature = self.sign(ws_response, key) - if data['h'] != signature: + if ws_response['h'] != signature: raise YubicoWSResponseBadSignature( "The signature sent by the server is invalid" ) @@ -115,15 +114,13 @@ class YubicoWS(object): def sign(self, data, key): "Signs the message with the provided key" - if 'h' in data: - # Just in case - data.pop('h') - - # Sorted k=v dict + # Sort k=v dict params = [] + for k in sorted(data.keys()): - key_value = "%s=%s" % (k, data[k]) - params.append(key_value) + if k != 'h': # Just in case + key_value = "%s=%s" % (k, data[k]) + params.append(key_value) # Join as urlparams parameters = '&'.join(params)