def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ options = options or {} message = 'Please confirm with your U2F token ({0!s})'.format( \ self.token.description) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) challenge = geturandom(32) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=None, challenge=binascii.hexlify(challenge), data=None, session=options.get("session"), validitytime=validity) db_challenge.save() sec_object = self.token.get_otpkey() key_handle_hex = sec_object.getKey() key_handle_bin = binascii.unhexlify(key_handle_hex) key_handle_url = url_encode(key_handle_bin) challenge_url = url_encode(challenge) u2f_sign_request = { "appId": self.get_tokeninfo("appId"), "version": U2F_Version, "challenge": challenge_url, "keyHandle": key_handle_url } image_url = IMAGES.get(self.token.description.lower().split()[0], "") response_details = { "u2fSignRequest": u2f_sign_request, "hideResponseInput": True, "img": image_url } return True, message, db_challenge.transaction_id, response_details
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional ``attributes``, which are displayed in the JSON response. """ options = options or {} message = get_action_values_from_options(SCOPE.AUTH, "{0!s}_{1!s}".format(self.get_class_type(), ACTION.CHALLENGETEXT), options)or _(u'Please confirm with your U2F token ({0!s})').format( self.token.description) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) challenge = geturandom(32) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=hexlify_and_unicode(challenge), data=None, session=options.get("session"), validitytime=validity) db_challenge.save() sec_object = self.token.get_otpkey() key_handle_hex = sec_object.getKey() key_handle_bin = binascii.unhexlify(key_handle_hex) key_handle_url = url_encode(key_handle_bin) challenge_url = url_encode(challenge) u2f_sign_request = {"appId": self.get_tokeninfo("appId"), "version": U2F_Version, "challenge": challenge_url, "keyHandle": key_handle_url} image_url = IMAGES.get(self.token.description.lower().split()[0], "") response_details = {"u2fSignRequest": u2f_sign_request, "hideResponseInput": True, "img": image_url} return True, message, db_challenge.transaction_id, response_details
def get_init_detail(self, params=None, user=None): """ At the end of the initialization we ask the user to press the button """ response_detail = {} if self.init_step == 1: # This is the first step of the init request app_id = get_from_config("u2f.appId", "").strip("/") from privacyidea.lib.error import TokenAdminError if not app_id: raise TokenAdminError( _("You need to define the appId in the " "token config!")) nonce = url_encode(geturandom(32)) response_detail = TokenClass.get_init_detail(self, params, user) register_request = { "version": U2F_Version, "challenge": nonce, "appId": app_id } response_detail["u2fRegisterRequest"] = register_request self.add_tokeninfo("appId", app_id) elif self.init_step == 2: # This is the second step of the init request response_detail["u2fRegisterResponse"] = { "subject": self.token.description } return response_detail
def get_init_detail(self, params=None, user=None): """ At the end of the initialization we ask the user to press the button """ response_detail = {} # get_init_details runs after "update" method. So in the first step clientwait has already been set if self.token.rollout_state == ROLLOUTSTATE.CLIENTWAIT: # This is the first step of the init request app_id = get_from_config("u2f.appId", "").strip("/") from privacyidea.lib.error import TokenAdminError if not app_id: raise TokenAdminError( _("You need to define the appId in the " "token config!")) nonce = url_encode(geturandom(32)) response_detail = TokenClass.get_init_detail(self, params, user) register_request = { "version": U2F_Version, "challenge": nonce, "appId": app_id } response_detail["u2fRegisterRequest"] = register_request self.add_tokeninfo("appId", app_id) elif self.token.rollout_state == "": # This is the second step of the init request, the clientwait rollout state has been reset response_detail["u2fRegisterResponse"] = { "subject": self.token.description } return response_detail
def check_otp(self, otpval, counter=None, window=None, options=None): """ This checks the response of a previous challenge. :param otpval: N/A :param counter: The authentication counter :param window: N/A :param options: contains "clientdata", "signaturedata" and "transaction_id" :return: A value > 0 in case of success """ ret = -1 clientdata = options.get("clientdata") signaturedata = options.get("signaturedata") transaction_id = options.get("transaction_id") # The challenge in the challenge DB object is saved in hex challenge = binascii.unhexlify(options.get("challenge", "")) if clientdata and signaturedata and transaction_id and challenge: # This is a valid response for a U2F token challenge_url = url_encode(challenge) clientdata = url_decode(clientdata) clientdata_dict = json.loads(clientdata) client_challenge = clientdata_dict.get("challenge") if challenge_url != client_challenge: raise ValidateError("Challenge mismatch. The U2F key did not " "send to original challenge.") if clientdata_dict.get("typ") != "navigator.id.getAssertion": raise ValidateError("Incorrect navigator.id") #client_origin = clientdata_dict.get("origin") signaturedata = url_decode(signaturedata) signaturedata_hex = binascii.hexlify(signaturedata) user_presence, counter, signature = parse_response_data( signaturedata_hex) user_pub_key = self.get_tokeninfo("pubKey") app_id = self.get_tokeninfo("appId") if check_response(user_pub_key, app_id, clientdata, binascii.hexlify(signature), counter, user_presence): # Signature verified. # check, if the counter increased! if counter > self.get_otp_count(): self.set_otp_count(counter) ret = counter else: log.warning("The signature of %s was valid, but contained " "an old counter." % self.token.serial) else: log.warning("Checking response for token {0!s} failed.".format( self.token.serial)) return ret
def check_otp(self, otpval, counter=None, window=None, options=None): """ This checks the response of a previous challenge. :param otpval: N/A :param counter: The authentication counter :param window: N/A :param options: contains "clientdata", "signaturedata" and "transaction_id" :return: A value > 0 in case of success """ ret = -1 clientdata = options.get("clientdata") signaturedata = options.get("signaturedata") transaction_id = options.get("transaction_id") # The challenge in the challenge DB object is saved in hex challenge = binascii.unhexlify(options.get("challenge", "")) if clientdata and signaturedata and transaction_id and challenge: # This is a valid response for a U2F token challenge_url = url_encode(challenge) clientdata = url_decode(clientdata) clientdata_dict = json.loads(clientdata) client_challenge = clientdata_dict.get("challenge") if challenge_url != client_challenge: return ret if clientdata_dict.get("typ") != "navigator.id.getAssertion": raise ValidateError("Incorrect navigator.id") #client_origin = clientdata_dict.get("origin") signaturedata = url_decode(signaturedata) signaturedata_hex = hexlify_and_unicode(signaturedata) user_presence, counter, signature = parse_response_data( signaturedata_hex) user_pub_key = self.get_tokeninfo("pubKey") app_id = self.get_tokeninfo("appId") if check_response(user_pub_key, app_id, clientdata, hexlify_and_unicode(signature), counter, user_presence): # Signature verified. # check, if the counter increased! if counter > self.get_otp_count(): self.set_otp_count(counter) ret = counter # At this point we can check, if the attestation # certificate is authorized. # If not, we can raise a policy exception g = options.get("g") user_object = self.user allowed_certs_pols = g.policy_object.get_action_values( U2FACTION.REQ, scope=SCOPE.AUTHZ, user_object=user_object if user_object else None, client=g.client_ip, audit_data=g.audit_object.audit_data) for allowed_cert in allowed_certs_pols: tag, matching, _rest = allowed_cert.split("/", 3) tag_value = self.get_tokeninfo( "attestation_{0!s}".format(tag)) # if we do not get a match, we bail out m = re.search(matching, tag_value) if not m: log.warning( "The U2F device {0!s} is not " "allowed to authenticate due to policy " "restriction".format(self.token.serial)) raise PolicyError("The U2F device is not allowed " "to authenticate due to policy " "restriction.") else: log.warning("The signature of %s was valid, but contained " "an old counter." % self.token.serial) else: log.warning("Checking response for token {0!s} failed.".format( self.token.serial)) return ret
def check_otp(self, otpval, counter=None, window=None, options=None): """ This checks the response of a previous challenge. :param otpval: N/A :param counter: The authentication counter :param window: N/A :param options: contains "clientdata", "signaturedata" and "transaction_id" :return: A value > 0 in case of success """ ret = -1 clientdata = options.get("clientdata") signaturedata = options.get("signaturedata") transaction_id = options.get("transaction_id") # The challenge in the challenge DB object is saved in hex challenge = binascii.unhexlify(options.get("challenge", "")) if clientdata and signaturedata and transaction_id and challenge: # This is a valid response for a U2F token challenge_url = url_encode(challenge) clientdata = url_decode(clientdata) clientdata_dict = json.loads(clientdata) client_challenge = clientdata_dict.get("challenge") if challenge_url != client_challenge: return ret if clientdata_dict.get("typ") != "navigator.id.getAssertion": raise ValidateError("Incorrect navigator.id") #client_origin = clientdata_dict.get("origin") signaturedata = url_decode(signaturedata) signaturedata_hex = hexlify_and_unicode(signaturedata) user_presence, counter, signature = parse_response_data( signaturedata_hex) user_pub_key = self.get_tokeninfo("pubKey") app_id = self.get_tokeninfo("appId") if check_response(user_pub_key, app_id, clientdata, hexlify_and_unicode(signature), counter, user_presence): # Signature verified. # check, if the counter increased! if counter > self.get_otp_count(): self.set_otp_count(counter) ret = counter # At this point we can check, if the attestation # certificate is authorized. # If not, we can raise a policy exception g = options.get("g") if self.user: token_user = self.user.login token_realm = self.user.realm token_resolver = self.user.resolver else: token_realm = token_resolver = token_user = None allowed_certs_pols = g.policy_object.get_action_values( U2FACTION.REQ, scope=SCOPE.AUTHZ, realm=token_realm, user=token_user, resolver=token_resolver, client=g.client_ip, audit_data=g.audit_object.audit_data) for allowed_cert in allowed_certs_pols: tag, matching, _rest = allowed_cert.split("/", 3) tag_value = self.get_tokeninfo( "attestation_{0!s}".format(tag)) # if we do not get a match, we bail out m = re.search(matching, tag_value) if not m: log.warning("The U2F device {0!s} is not " "allowed to authenticate due to policy " "restriction".format( self.token.serial)) raise PolicyError("The U2F device is not allowed " "to authenticate due to policy " "restriction.") else: log.warning("The signature of %s was valid, but contained " "an old counter." % self.token.serial) else: log.warning("Checking response for token {0!s} failed.".format( self.token.serial)) return ret
def test_02_validate(self): # assign token to user r = assign_token(self.serial, User("cornelius", self.realm1), pin="u2f") self.assertEqual(r, True) # Issue challenge with self.app.test_request_context('/validate/check', method='POST', data={ "user": "******" + self.realm1, "pass": "******" }): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) result = json.loads(res.data).get("result") detail = json.loads(res.data).get("detail") self.assertEqual(result.get("value"), False) transaction_id = detail.get("transaction_id") self.assertEqual(len(transaction_id), len('01350277175811850842')) self.assertTrue( "Please confirm with your U2F token" in detail.get("message"), detail.get("message")) attributes = detail.get("attributes") u2f_sign_request = attributes.get("u2fSignRequest") self.assertTrue("appId" in u2f_sign_request) app_id = u2f_sign_request.get("appId") self.assertTrue("challenge" in u2f_sign_request) challenge = u2f_sign_request.get("challenge") self.assertTrue("keyHandle" in u2f_sign_request) key_handle = u2f_sign_request.get("keyHandle") self.assertEqual(u2f_sign_request.get("version"), "U2F_V2") # private key from the registration example privkey = "9a9684b127c5e3a706d618c86401c7cf6fd827fd0bc18d24b0eb842e36d16df1" counter = 1 client_data = '{"typ":"navigator.id.getAssertion",' \ '"challenge":"%s","cid_pubkey":{"kty":"EC",' \ '"crv":"P-256",' \ '"x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8",' \ '"y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},' \ '"origin":"%s"}' % (challenge, app_id) signature_hex = sign_challenge(privkey, app_id, client_data, counter) signature_data_hex = "0100000001" + signature_hex signature_data_url = url_encode(binascii.unhexlify(signature_data_hex)) client_data_url = url_encode(client_data) # Send the response. Unfortunately it does not fit to the # registration, so we create a BadSignatureError with self.app.test_request_context('/validate/check', method='POST', data={ "user": "******", "realm": self.realm1, "pass": "", "transaction_id": transaction_id, "clientdata": client_data_url, "signaturedata": signature_data_url }): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) result = json.loads(res.data).get("result") detail = json.loads(res.data).get("detail") self.assertEqual(result.get("status"), True) self.assertEqual(result.get("value"), False)
def create_challenge(self, transactionid=None, options=None): """ This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database. If no transaction id is given, the system will create a transaction id and return it, so that the response can refer to this transaction. :param transactionid: the id of this challenge :param options: the request context parameters / data :type options: dict :return: tuple of (bool, message, transactionid, attributes) :rtype: tuple The return tuple builds up like this: ``bool`` if submit was successful; ``message`` which is displayed in the JSON response; additional challenge ``reply_dict``, which are displayed in the JSON challenges response. """ options = options or {} message = get_action_values_from_options( SCOPE.AUTH, "{0!s}_{1!s}".format( self.get_class_type(), ACTION.CHALLENGETEXT), options) or _( u'Please confirm with your U2F token ({0!s})').format( self.token.description) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # if a transaction id is given, check if there are other u2f token and # reuse the challenge challenge = None if transactionid: for c in get_challenges(transaction_id=transactionid): if get_tokens(serial=c.serial, tokentype=self.get_class_type(), count=True): challenge = c.challenge break if not challenge: nonce = geturandom(32) challenge = hexlify_and_unicode(nonce) else: nonce = binascii.unhexlify(challenge) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=None, session=options.get("session"), validitytime=validity) db_challenge.save() sec_object = self.token.get_otpkey() key_handle_hex = sec_object.getKey() key_handle_bin = binascii.unhexlify(key_handle_hex) key_handle_url = url_encode(key_handle_bin) challenge_url = url_encode(nonce) u2f_sign_request = { "appId": self.get_tokeninfo("appId"), "version": U2F_Version, "challenge": challenge_url, "keyHandle": key_handle_url } image_url = IMAGES.get(self.token.description.lower().split()[0], "") reply_dict = { "attributes": { "u2fSignRequest": u2f_sign_request, "hideResponseInput": self.client_mode != CLIENTMODE.INTERACTIVE, "img": image_url }, "image": image_url } return True, message, db_challenge.transaction_id, reply_dict
def test_02_validate(self): # assign token to user r = assign_token(self.serial, User("cornelius", self.realm1), pin="u2f") self.assertEqual(r, True) # Issue challenge with self.app.test_request_context('/validate/check', method='POST', data={"user": "******"+self.realm1, "pass": "******"}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) result = json.loads(res.data).get("result") detail = json.loads(res.data).get("detail") self.assertEqual(result.get("value"), False) transaction_id = detail.get("transaction_id") self.assertEqual(len(transaction_id), len('01350277175811850842')) self.assertTrue("Please confirm with your U2F token" in detail.get("message"), detail.get("message")) attributes = detail.get("attributes") u2f_sign_request = attributes.get("u2fSignRequest") self.assertTrue("appId" in u2f_sign_request) app_id = u2f_sign_request.get("appId") self.assertTrue("challenge" in u2f_sign_request) challenge = u2f_sign_request.get("challenge") self.assertTrue("keyHandle" in u2f_sign_request) key_handle = u2f_sign_request.get("keyHandle") self.assertEqual(u2f_sign_request.get("version"), "U2F_V2") # private key from the registration example privkey = "9a9684b127c5e3a706d618c86401c7cf6fd827fd0bc18d24b0eb842e36d16df1" counter = 1 client_data = '{"typ":"navigator.id.getAssertion",' \ '"challenge":"%s","cid_pubkey":{"kty":"EC",' \ '"crv":"P-256",' \ '"x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8",' \ '"y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},' \ '"origin":"%s"}' % (challenge, app_id) signature_hex = sign_challenge(privkey, app_id, client_data, counter) signature_data_hex = "0100000001" + signature_hex signature_data_url = url_encode(binascii.unhexlify(signature_data_hex)) client_data_url = url_encode(client_data) # Send the response. Unfortunately it does not fit to the # registration, so we create a BadSignatureError with self.app.test_request_context('/validate/check', method='POST', data={"user": "******", "realm": self.realm1, "pass": "", "transaction_id": transaction_id, "clientdata": client_data_url, "signaturedata": signature_data_url}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) result = json.loads(res.data).get("result") detail = json.loads(res.data).get("detail") self.assertEqual(result.get("status"), True) self.assertEqual(result.get("value"), False)