def test_05_encode_decode(self): b_str = b'Hello World' self.assertEqual(to_unicode(b_str), b_str.decode('utf8')) u_str = u'Hello Wörld' self.assertEqual(to_unicode(u_str), u_str) self.assertEqual(to_bytes(b_str), b_str) self.assertEqual(to_bytes(u_str), u_str.encode('utf8'))
def _on_Connection(self, server, user, password, auto_bind=None, client_strategy=None, authentication=None, check_names=None, auto_referrals=None, receive_timeout=None): """ We need to create a Connection object with methods: add() modify() search() unbind() and object response """ # check the password correct_password = False # Anonymous bind # Reload the directory just in case a change has been made to # user credentials self.directory = self._load_data(DIRECTORY) if authentication == ldap3.ANONYMOUS and user == "": correct_password = True for entry in self.directory: if entry.get("dn") == user: pw = entry.get("attributes").get("userPassword") # password can be unicode if to_bytes(pw) == to_bytes(password): correct_password = True elif pw.startswith('{SSHA}'): correct_password = ldap_salted_sha1.verify(password, pw) else: correct_password = False self.con_obj = Connection(self.directory) self.con_obj.bound = correct_password return self.con_obj
def check_response(user_pub_key, app_id, client_data, signature, counter, user_presence_byte=b'\x01'): """ Check the ECDSA Signature with the given pubkey. The signed data is constructed from * app_id * user_presence_byte * counter and * client_data :param user_pub_key: The Application specific public key :type user_pub_key: hex string :param app_id: The AppID for this challenge response :type app_id: str :param client_data: The ClientData :type client_data: str :param counter: A counter :type counter: int :param user_presence_byte: User presence byte :type user_presence_byte: byte :param signature: The signature of the authentication request :type signature: hex string :return: """ res = True app_id_hash = sha256(to_bytes(app_id)).digest() client_data_hash = sha256(to_bytes(client_data)).digest() user_pub_key_bin = binascii.unhexlify(user_pub_key) counter_bin = struct.pack(">L", counter) signature_bin = binascii.unhexlify(signature) input_data = app_id_hash + user_presence_byte + counter_bin \ + client_data_hash # The first byte 0x04 only indicates, that the public key is in the # uncompressed format x: 32 byte, y: 32byte user_pub_key_bin = user_pub_key_bin[1:] signature_bin_asn = der_decode(signature_bin) vkey = ecdsa.VerifyingKey.from_string(user_pub_key_bin, curve=ecdsa.NIST256p, hashfunc=sha256) try: vkey.verify(signature_bin_asn, input_data) except ecdsa.BadSignatureError: log.error("Bad signature for app_id {0!s}".format(app_id)) res = False return res
def encryptPin(cryptPin): """ :param cryptPin: the pin to encrypt :type cryptPin: bytes or str :return: the encrypted pin :rtype: str """ hsm = get_hsm() return to_unicode(hsm.encrypt_pin(to_bytes(cryptPin)))
def test_01_check_reg_data(self): attestation_cert = "3082013c3081e4a003020102020a4790128000115595735230" \ "0a06082a8648ce3d0403023017311530130603550403130c" \ "476e756262792050696c6f74301e170d313230383134313" \ "8323933325a170d3133303831343138323933325a303131" \ "2f302d0603550403132650696c6f74476e756262792d302" \ "e342e312d34373930313238303030313135353935373335" \ "323059301306072a8648ce3d020106082a8648ce3d030107" \ "034200048d617e65c9508e64bcc5673ac82a6799da3c1446" \ "682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4" \ "199edd7862f23abaf0203b4b8911ba0569994e101300a06" \ "082a8648ce3d0403020347003044022060cdb6061e9c2226" \ "2d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa" \ "0220631b1459f09e6330055722c8d89b7f48883b9089b88d" \ "60d1d9795902b30410df" cdata_str = """{"typ": "navigator.id.finishEnrollment", "challenge": "vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo", "cid_pubkey": { "kty": "EC", "crv": "P-256", "x": "HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8", "y": "XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"}, "origin": "http://example.com"}""" my_app_id = 'http://example.com' app_id_hash = hexlify_and_unicode(sha256(to_bytes(my_app_id)).digest()) self.assertEqual(app_id_hash, "f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4") attestation_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, binascii.unhexlify(attestation_cert)) client_data_str = ''.join(cdata_str.split()) client_data_hash = hexlify_and_unicode(sha256(to_bytes(client_data_str)).digest()) self.assertEqual(client_data_hash, "4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb") user_pub_key = "04b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9" key_handle = "2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25" signature = "304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" r = check_registration_data(attestation_cert, my_app_id, client_data_str, user_pub_key, key_handle, signature) self.assertEqual(r, True)
def sign_challenge(user_priv_key, app_id, client_data, counter, user_presence_byte=b'\x01'): """ This creates a signature for the U2F data. Only used in test scenario The calculation of the signature is described here: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#authentication-response-message-success The input_data is a concatenation of: * AppParameter: sha256(app_id) * The user presence [1byte] * counter [4byte] * ChallengeParameter: sha256(client_data) :param user_priv_key: The private key :type user_priv_key: hex string :param app_id: The application id :type app_id: str :param client_data: the stringified JSON :type client_data: str :param counter: the authentication counter :type counter: int :param user_presence_byte: one byte 0x01 :type user_presence_byte: char :return: The DER encoded signature :rtype: hex string """ app_id_hash = sha256(to_bytes(app_id)).digest() client_data_hash = sha256(to_bytes(client_data)).digest() counter_bin = struct.pack(">L", counter) input_data = app_id_hash + user_presence_byte + counter_bin + \ client_data_hash priv_key_bin = binascii.unhexlify(user_priv_key) sk = ecdsa.SigningKey.from_string(priv_key_bin, curve=ecdsa.NIST256p, hashfunc=sha256) signature = sk.sign(input_data) der_sig = der_encode(signature) return hexlify_and_unicode(der_sig)
def test_25_encodings(self): u = u'Hello Wörld' b = b'Hello World' self.assertEquals(to_utf8(None), None) self.assertEquals(to_utf8(u), u.encode('utf8')) self.assertEquals(to_utf8(b), b) self.assertEquals(to_unicode(u), u) self.assertEquals(to_unicode(b), b.decode('utf8')) self.assertEquals(to_unicode(None), None) self.assertEquals(to_unicode(10), 10) self.assertEquals(to_bytes(u), u.encode('utf8')) self.assertEquals(to_bytes(b), b) self.assertEquals(to_bytes(10), 10) self.assertEquals(to_byte_string(u), u.encode('utf8')) self.assertEquals(to_byte_string(b), b) self.assertEquals(to_byte_string(10), b'10')
def url_decode(url): """ Decodes a base64 encoded, not padded string as used in FIDO U2F :param url: base64 urlsafe encoded string :type url: str :return: the decoded string :rtype: bytes """ pad_len = len(url) % 4 padding = pad_len * "=" res = base64.urlsafe_b64decode(to_bytes(url + padding)) return res
def url_decode(url): """ Decodes a base64 encoded, not padded string as used in FIDO U2F :param url: base64 urlsafe encoded string :type url: str :return: the decoded string :rtype: bytes """ pad_len = -len(url) % 4 padding = pad_len * "=" res = base64.urlsafe_b64decode(to_bytes(url + padding)) return res
def create_key_from_password(password): """ Create a key from the given password. This is used to encrypt and decrypt the enckey file. :param password: :type password: str or bytes :return: the generated key :rtype: bytes """ key = sha256(to_bytes(password)).digest()[0:32] return key
def _build_verify_object(pubkey_pem): """ Load the given stripped and urlsafe public key and return the verify object :param pubkey_pem: :return: """ # The public key of the smartphone was probably sent as urlsafe: pubkey_pem = pubkey_pem.replace("-", "+").replace("_", "/") # The public key was sent without any header pubkey_pem = "-----BEGIN PUBLIC KEY-----\n{0!s}\n-----END PUBLIC " \ "KEY-----".format(pubkey_pem.strip().replace(" ", "+")) return serialization.load_pem_public_key(to_bytes(pubkey_pem), default_backend())
def _digi2daplug(normal_otp): """ convert "497096" to 34 39 37 30 39 36, which is efekeiebekeh This function is only used for testing purposes :param normal_otp: :type normal_otp: bytes or str :return: """ daplug_otp = "" hex_otp = hexlify_and_unicode(to_bytes(normal_otp)) for i in hex_otp: daplug_otp += REVERSE_MAPPING.get(i) return daplug_otp
def get_access_token(server=None, client=None, secret=None): auth = to_unicode(base64.b64encode(to_bytes(client + ':' + secret))) url = "{0!s}/oauth/token?grant_type=client_credentials".format(server) resp = requests.get(url, headers={'Authorization': 'Basic ' + auth}) if resp.status_code != 200: info = "Could not get access token: {0!s}".format(resp.status_code) log.error(info) raise Exception(info) access_token = yaml.safe_load(resp.content).get('access_token') return access_token
def check_registration_data(attestation_cert, app_id, client_data, user_pub_key, key_handle, signature): """ See example in fido spec https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#registration-example In case of signature error an exception is raised :param attestation_cert: The Attestation cert of the FIDO device :type attestation_cert: x509 Object :param app_id: The appId :type app_id: str :param client_data: The ClientData :type client_data: str :param user_pub_key: The public key for this AppID :type user_pub_key: hex string :param key_handle: The keyHandle on the FIDO device :type key_handle: hex string :param signature: The signature of the registration request :type signature: hex string :return: Bool """ app_id_hash = sha256(to_bytes(app_id)).digest() client_data_hash = sha256(to_bytes(client_data)).digest() reg_data = b'\x00' + app_id_hash + client_data_hash \ + binascii.unhexlify(key_handle) + binascii.unhexlify(user_pub_key) try: crypto.verify(attestation_cert, binascii.unhexlify(signature), reg_data, "sha256") except Exception as exx: raise Exception("Error checking the signature of the registration " "data. %s" % exx) return True
def _build_smartphone_data(serial, challenge, fb_gateway, pem_privkey, options): """ Create the dictionary to be send to the smartphone as challenge :param challenge: base32 encoded random data string :type challenge: str :param fb_gateway: the gateway object containing the firebase configuration :type fb_gateway: privacyidea.lib.smsprovider.SMSProvider.ISMSProvider :param options: the options dictionary :type options: dict :return: the created smartphone_data dictionary :rtype: dict """ sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") # We send the challenge to the Firebase service url = fb_gateway.smsgateway.option_dict.get( FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": serial, "title": title, "sslverify": sslverify, "url": url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) return smartphone_data
def check_firebase_params(request): payload = json.loads(request.body) # check the signature in the payload! data = payload.get("message").get("data") sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(**data) token_obj = get_tokens(serial=data.get("serial"))[0] pem_pubkey = token_obj.get_tokeninfo(PUBLIC_KEY_SERVER) pubkey_obj = load_pem_public_key(to_bytes(pem_pubkey), backend=default_backend()) signature = b32decode(data.get("signature")) # If signature does not match it will raise InvalidSignature exception pubkey_obj.verify(signature, sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} return (200, headers, json.dumps({}))
def _build_smartphone_data(serial, challenge, registration_url, pem_privkey, options): """ Create the dictionary to be send to the smartphone as challenge :param challenge: base32 encoded random data string :type challenge: str :param registration_url: The privacyIDEA URL, to which the Push token communicates :type registration_url: str :param options: the options dictionary :type options: dict :return: the created smartphone_data dictionary :rtype: dict """ sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": serial, "title": title, "sslverify": sslverify, "url": registration_url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) return smartphone_data
def checkPass(self, uid, password): """ This function checks the password for a given uid. returns true in case of success false if password does not match We do not support shadow passwords. so the seconds column of the passwd file needs to contain the encrypted password If the password is a unicode object, it is encoded according to ENCODING first. :param uid: The uid of the user :type uid: int :param password: The password in cleartext :type password: sting :return: True or False :rtype: bool """ log.info("checking password for user uid {0!s}".format(uid)) if six.PY2: # crypt needs bytes in python 2 password = to_bytes(password) cryptedpasswd = self.passDict[uid] log.debug("We found the encrypted pass {0!s} for uid {1!s}".format( cryptedpasswd, uid)) if cryptedpasswd: if cryptedpasswd in ['x', '*']: err = "Sorry, currently no support for shadow passwords" log.error("{0!s}".format(err)) raise NotImplementedError(err) cp = crypt.crypt(password, cryptedpasswd) log.debug("encrypted pass is {0!s}".format(cp)) if crypt.crypt(password, cryptedpasswd) == cryptedpasswd: log.info( "successfully authenticated user uid {0!s}".format(uid)) return True else: log.warning( "user uid {0!s} failed to authenticate".format(uid)) return False else: log.warning("Failed to verify password. No encrypted password " "found in file") return False
def encryptPassword(password): """ Encrypt given password with hsm This function returns a unicode string with a hexlified contents of the IV and the encrypted data separated by a colon like u"4956:44415441" :param password: the password :type password: bytes or str :return: the encrypted password, hexlified :rtype: str """ hsm = get_hsm() try: ret = hsm.encrypt_password(to_bytes(password)) except Exception as exx: # pragma: no cover log.warning(exx) ret = "FAILED TO ENCRYPT PASSWORD!" return ret
def _encrypt_value(self, value, slot_id): """ base method to encrypt a value - uses one slot id to encrypt a string returns as string with leading iv, separated by ':' :param value: the value that is to be encrypted :type value: str :param slot_id: slot of the key array :type slot_id: int :return: encrypted data with leading iv and separator ':' :rtype: str """ iv = self.random(16) v = self.encrypt(to_bytes(value), iv, slot_id) cipher_value = binascii.hexlify(iv) + b':' + binascii.hexlify(v) return cipher_value.decode("utf-8")
def sign(self, s): """ Create a signature of the string s :param s: String to sign :type s: str :return: The hexlified and versioned signature of the string :rtype: str """ if not self.private: log.info('Could not sign message {0!s}, no private key!'.format(s)) # TODO: should we throw an exception in this case? return '' signature = self.private.sign( to_bytes(s), asym_padding.PSS(mgf=asym_padding.MGF1(hashes.SHA256()), salt_length=asym_padding.PSS.MAX_LENGTH), hashes.SHA256()) res = ':'.join([self.sig_ver, hexlify_and_unicode(signature)]) return res
def check_password(self, password): """ :param password: :type password: str :return: result of password check: 0 if success, -1 if failed :rtype: int """ res = -1 key = self.secretObject.getKey() # getKey() returns bytes and since we can not assume, that the # password only contains printable characters, we need to compare # bytes strings here. This also avoids making another copy of 'key'. if key == to_bytes(password): res = 0 zerome(key) del key return res
def calcOtp(self, counter, key=None, pin=None): ''' calculate an otp value from counter/time, key and pin :param counter: counter/time to be checked :type counter: int :param key: the secret key :type key: str :param pin: the secret pin :type pin: str :return: the otp value :rtype: str ''' ## ref impl from https://github.com/szimszon/motpy/blob/master/motpy if pin is None: pin = self.pin if key is None: key = self.key vhash = u"{0:d}{1!s}{2!s}".format(counter, key, pin) motp = md5(to_bytes(vhash)).hexdigest()[:self.digits] return to_unicode(motp)
def yubico_api_signature(data, api_key): """ Get a dictionary "data", sort the dictionary by the keys and sign it HMAC-SHA1 with the api_key :param data: The data to be signed :type data: dict :param api_key: base64 encoded API key :type api_key: basestring :return: base64 encoded signature """ r = dict(data) if 'h' in r: del r['h'] keys = sorted(r.keys()) data_string = "" for key in keys: data_string += "{0!s}={1!s}&".format(key, r.get(key)) data_string = data_string.strip("&") api_key_bin = base64.b64decode(api_key) # generate the signature h = hmac.new(api_key_bin, to_bytes(data_string), sha1).digest() h_b64 = b64encode_and_unicode(h) return h_b64
def test_02_api_enroll(self): self.authenticate() # Failed enrollment due to missing policy with self.app.test_request_context('/token/init', method='POST', data={"type": "push", "genkey": 1}, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertNotEqual(res.status_code, 200) error = json.loads(res.data.decode("utf8")).get("result").get("error") self.assertEqual(error.get("message"), "Missing enrollment policy for push token: push_firebase_configuration") self.assertEqual(error.get("code"), 303) r = set_smsgateway(self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", {FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.APP_ID: "2", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "test-123456", FIREBASE_CONFIG.JSON_CONFIG: FIREBASE_FILE}) self.assertTrue(r > 0) set_policy("push1", scope=SCOPE.ENROLL, action="{0!s}={1!s}".format(PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name)) # 1st step with self.app.test_request_context('/token/init', method='POST', data={"type": "push", "genkey": 1}, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = json.loads(res.data.decode('utf8')).get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertTrue("pushurl" in detail) # check that the new URL contains the serial number self.assertTrue("&serial=PIPU" in detail.get("pushurl").get("value")) self.assertTrue("appid=" in detail.get("pushurl").get("value")) self.assertTrue("appidios=" in detail.get("pushurl").get("value")) self.assertTrue("apikeyios=" in detail.get("pushurl").get("value")) self.assertFalse("otpkey" in detail) enrollment_credential = detail.get("enrollment_credential") # 2nd step. Failing with wrong serial number with self.app.test_request_context('/ttype/push', method='POST', data={"serial": "wrongserial", "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT"}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 404, res) status = json.loads(res.data.decode('utf8')).get("result").get("status") self.assertFalse(status) error = json.loads(res.data.decode('utf8')).get("result").get("error") self.assertEqual(error.get("message"), "No token with this serial number in the rollout state 'clientwait'.") # 2nd step. Fails with missing enrollment credential with self.app.test_request_context('/ttype/push', method='POST', data={"serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT", "enrollment_credential": "WRonG"}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 400, res) status = json.loads(res.data.decode('utf8')).get("result").get("status") self.assertFalse(status) error = json.loads(res.data.decode('utf8')).get("result").get("error") self.assertEqual(error.get("message"), "ERR905: Invalid enrollment credential. You are not authorized to finalize this token.") # 2nd step: as performed by the smartphone with self.app.test_request_context('/ttype/push', method='POST', data={"enrollment_credential": enrollment_credential, "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT"}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) detail = json.loads(res.data.decode('utf8')).get("detail") # still the same serial number self.assertEqual(serial, detail.get("serial")) self.assertEqual(detail.get("rollout_state"), "enrolled") # Now the smartphone gets a public key from the server augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) pubkey = detail.get("public_key") # Now check, what is in the token in the database toks = get_tokens(serial=serial) self.assertEqual(len(toks), 1) token_obj = toks[0] self.assertEqual(token_obj.token.rollout_state, u"enrolled") self.assertTrue(token_obj.token.active) tokeninfo = token_obj.get_tokeninfo() self.assertEqual(tokeninfo.get("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertEqual(tokeninfo.get("firebase_token"), u"firebaseT") self.assertEqual(tokeninfo.get("public_key_server").strip().strip("-BEGIN END RSA PUBLIC KEY-").strip(), pubkey) # The token should also contain the firebase config self.assertEqual(tokeninfo.get(PUSH_ACTION.FIREBASE_CONFIG), self.firebase_config_name)
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. """ res = False options = options or {} message = get_action_values_from_options( SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") attributes = None data = None challenge = b32encode_and_unicode(geturandom()) fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: # We send the challenge to the Firebase service fb_gateway = create_sms_instance(fb_identifier) url = fb_gateway.smsgateway.option_dict.get( FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": self.token.serial, "title": title, "sslverify": sslverify, "url": url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) privkey_obj = serialization.load_pem_private_key( to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) res = fb_gateway.submit_message( self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError( "Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format( self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError( "The token has no tokeninfo. Can not send via firebase service." ) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes
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 answer the challenge' attributes = {} # Get ValidityTime=120s. Maybe there is a OCRAChallengeValidityTime... 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)) # Get the OCRASUITE from the token information ocrasuite = self.get_tokeninfo("ocrasuite") or OCRA_DEFAULT_SUITE challenge = options.get("challenge") # TODO: we could add an additional parameter to hash the challenge # cleartext -> sha1 if not challenge: # If no challenge is given in the Request, we create a random # challenge based on the OCRA-SUITE os = OCRASuite(ocrasuite) challenge = os.create_challenge() else: # Add a random challenge if options.get("addrandomchallenge"): challenge += get_alphanum_str(int(options.get( "addrandomchallenge"))) attributes["original_challenge"] = challenge attributes["qrcode"] = create_img(challenge) if options.get("hashchallenge", "").lower() == "sha256": challenge = hexlify_and_unicode(hashlib.sha256(to_bytes(challenge)).digest()) elif options.get("hashchallenge", "").lower() == "sha512": challenge = hexlify_and_unicode(hashlib.sha512(to_bytes(challenge)).digest()) elif options.get("hashchallenge"): challenge = hexlify_and_unicode(hashlib.sha1(to_bytes(challenge)).digest()) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=None, challenge=challenge, data=None, session=None, validitytime=validity) db_challenge.save() attributes["challenge"] = challenge return True, message, db_challenge.transaction_id, attributes
def api_endpoint(cls, request, g): """ This provides a function which is called by the API endpoint /ttype/push which is defined in api/ttype.py The method returns return "json", {} This endpoint is used for the 2nd enrollment step of the smartphone. Parameters sent: * serial * fbtoken * pubkey This endpoint is also used, if the smartphone sends the signed response to the challenge during authentication Parameters sent: * serial * nonce (which is the challenge) * signature (which is the signed nonce) :param request: The Flask request :param g: The Flask global object g :return: dictionary """ details = {} result = False serial = getParam(request.all_data, "serial", optional=False) if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data: # Do the 2nd step of the enrollment try: token_obj = get_one_token(serial=serial, tokentype="push", rollout_state="clientwait") token_obj.update(request.all_data) except ResourceNotFoundError: raise ResourceNotFoundError( "No token with this serial number in the rollout state 'clientwait'." ) init_detail_dict = request.all_data details = token_obj.get_init_detail(init_detail_dict) result = True elif serial and "nonce" in request.all_data and "signature" in request.all_data: challenge = getParam(request.all_data, "nonce") serial = getParam(request.all_data, "serial") signature = getParam(request.all_data, "signature") # get the token_obj for the given serial: token_obj = get_one_token(serial=serial, tokentype="push") pubkey_pem = token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE) # The public key of the smartphone was probably sent as urlsafe: pubkey_pem = pubkey_pem.replace("-", "+").replace("_", "/") # The public key was sent without any header pubkey_pem = "-----BEGIN PUBLIC KEY-----\n{0!s}\n-----END PUBLIC KEY-----".format( pubkey_pem.strip().replace(" ", "+")) # Do the 2nd step of the authentication # Find valid challenges challengeobject_list = get_challenges(serial=serial, challenge=challenge) if challengeobject_list: # There are valid challenges, so we check this signature for chal in challengeobject_list: # verify the signature of the nonce pubkey_obj = serialization.load_pem_public_key( to_bytes(pubkey_pem), default_backend()) sign_data = u"{0!s}|{1!s}".format(challenge, serial) try: pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid chal.set_otp_status(True) result = True except InvalidSignature as e: pass else: raise ParameterError("Missing parameters!") return "json", prepare_result(result, details=details)
def test_01_create_token(self): db_token = Token(self.serial1, tokentype="push") db_token.save() token = PushTokenClass(db_token) self.assertEqual(token.token.serial, self.serial1) self.assertEqual(token.token.tokentype, "push") self.assertEqual(token.type, "push") class_prefix = token.get_class_prefix() self.assertEqual(class_prefix, "PIPU") self.assertEqual(token.get_class_type(), "push") # Test to do the 2nd step, although the token is not yet in clientwait self.assertRaises(ParameterError, token.update, {"otpkey": "1234", "pubkey": "1234", "serial": self.serial1}) # Run enrollment step 1 token.update({"genkey": 1}) # Now the token is in the state clientwait, but insufficient parameters would still fail self.assertRaises(ParameterError, token.update, {"otpkey": "1234"}) self.assertRaises(ParameterError, token.update, {"otpkey": "1234", "pubkey": "1234"}) # Unknown config self.assertRaises(ParameterError, token.get_init_detail, params={"firebase_config": "bla"}) fb_config = {FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.APP_ID: "2", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4"} # Wrong JSON file self.assertRaises(ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) # Wrong Project number fb_config[FIREBASE_CONFIG.JSON_CONFIG] = FIREBASE_FILE self.assertRaises(ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) # Missing APP_ID self.assertRaises(ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", {FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4"}) # Missing API_KEY_IOS self.assertRaises(ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", {FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.APP_ID_IOS: "1", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4"}) # Everything is fine fb_config[FIREBASE_CONFIG.PROJECT_ID] = "test-123456" r = set_smsgateway("fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) self.assertTrue(r > 0) detail = token.get_init_detail(params={"firebase_config": self.firebase_config_name}) self.assertEqual(detail.get("serial"), self.serial1) self.assertEqual(detail.get("rollout_state"), "clientwait") enrollment_credential = detail.get("enrollment_credential") self.assertTrue("pushurl" in detail) self.assertFalse("otpkey" in detail) # Run enrollment step 2 token.update({"enrollment_credential": enrollment_credential, "serial": self.serial1, "fbtoken": "firebasetoken", "pubkey": self.smartphone_public_key_pem_urlsafe}) self.assertEqual(token.get_tokeninfo("firebase_token"), "firebasetoken") self.assertEqual(token.get_tokeninfo("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertTrue(token.get_tokeninfo("public_key_server").startswith(u"-----BEGIN RSA PUBLIC KEY-----\n"), token.get_tokeninfo("public_key_server")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(token.get_tokeninfo("public_key_server")), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) self.assertTrue(token.get_tokeninfo("private_key_server").startswith(u"-----BEGIN RSA PRIVATE KEY-----\n"), token.get_tokeninfo("private_key_server")) parsed_server_privkey = serialization.load_pem_private_key( to_bytes(token.get_tokeninfo("private_key_server")), None, default_backend()) self.assertIsInstance(parsed_server_privkey, RSAPrivateKey) detail = token.get_init_detail() self.assertEqual(detail.get("rollout_state"), "enrolled") augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_stripped_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertEqual(parsed_server_pubkey.public_numbers(), parsed_stripped_server_pubkey.public_numbers()) remove_token(self.serial1)
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, reply_dict) :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 = 'Please answer the challenge' attributes = {} # Get ValidityTime=120s. Maybe there is a OCRAChallengeValidityTime... 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)) # Get the OCRASUITE from the token information ocrasuite = self.get_tokeninfo("ocrasuite") or OCRA_DEFAULT_SUITE challenge = options.get("challenge") # TODO: we could add an additional parameter to hash the challenge # cleartext -> sha1 if not challenge: # If no challenge is given in the Request, we create a random # challenge based on the OCRA-SUITE os = OCRASuite(ocrasuite) challenge = os.create_challenge() else: # Add a random challenge if options.get("addrandomchallenge"): challenge += get_alphanum_str( int(options.get("addrandomchallenge"))) attributes["original_challenge"] = challenge attributes["qrcode"] = create_img(challenge) if options.get("hashchallenge", "").lower() == "sha256": challenge = hexlify_and_unicode( hashlib.sha256(to_bytes(challenge)).digest()) elif options.get("hashchallenge", "").lower() == "sha512": challenge = hexlify_and_unicode( hashlib.sha512(to_bytes(challenge)).digest()) elif options.get("hashchallenge"): challenge = hexlify_and_unicode( hashlib.sha1(to_bytes(challenge)).digest()) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=None, challenge=challenge, data=None, session=None, validitytime=validity) db_challenge.save() attributes["challenge"] = challenge reply_dict = {"attributes": attributes} return True, message, db_challenge.transaction_id, reply_dict
def _check_radius(self, otpval, options=None, radius_state=None): """ run the RADIUS request against the RADIUS server :param otpval: the OTP value :param options: additional token specific options :type options: dict :return: counter of the matching OTP value. :rtype: AccessAccept, AccessReject, AccessChallenge """ result = AccessReject radius_message = None if options is None: options = {} radius_dictionary = None radius_identifier = self.get_tokeninfo("radius.identifier") radius_user = self.get_tokeninfo("radius.user") system_radius_settings = self.get_tokeninfo("radius.system_settings") radius_timeout = 5 radius_retries = 3 if radius_identifier: # New configuration radius_server_object = get_radius(radius_identifier) radius_server = radius_server_object.config.server radius_port = radius_server_object.config.port radius_server = u"{0!s}:{1!s}".format(radius_server, radius_port) radius_secret = radius_server_object.get_secret() radius_dictionary = radius_server_object.config.dictionary radius_timeout = int(radius_server_object.config.timeout or 10) radius_retries = int(radius_server_object.config.retries or 1) elif system_radius_settings: # system configuration radius_server = get_from_config("radius.server") radius_secret = get_from_config("radius.secret") else: # individual token settings radius_server = self.get_tokeninfo("radius.server") # Read the secret secret = self.token.get_otpkey() radius_secret = binascii.unhexlify(secret.getKey()) # here we also need to check for radius.user log.debug(u"checking OTP len:{0!s} on radius server: " u"{1!s}, user: {2!r}".format(len(otpval), radius_server, radius_user)) try: # pyrad does not allow to set timeout and retries. # it defaults to retries=3, timeout=5 # TODO: At the moment we support only one radius server. # No round robin. server = radius_server.split(':') r_server = server[0] r_authport = 1812 if len(server) >= 2: r_authport = int(server[1]) nas_identifier = get_from_config("radius.nas_identifier", "privacyIDEA") if not radius_dictionary: radius_dictionary = get_from_config( "radius.dictfile", "/etc/privacyidea/dictionary") log.debug(u"NAS Identifier: %r, " u"Dictionary: %r" % (nas_identifier, radius_dictionary)) log.debug(u"constructing client object " u"with server: %r, port: %r, secret: %r" % (r_server, r_authport, to_unicode(radius_secret))) srv = Client(server=r_server, authport=r_authport, secret=to_bytes(radius_secret), dict=Dictionary(radius_dictionary)) # Set retries and timeout of the client srv.timeout = radius_timeout srv.retries = radius_retries req = srv.CreateAuthPacket( code=pyrad.packet.AccessRequest, User_Name=radius_user.encode('utf-8'), NAS_Identifier=nas_identifier.encode('ascii')) req["User-Password"] = req.PwCrypt(otpval) if radius_state: req["State"] = radius_state log.info( u"Sending saved challenge to radius server: {0!r} ".format( radius_state)) try: response = srv.SendPacket(req) except Timeout: log.warning( u"The remote RADIUS server {0!s} timeout out for user {1!s}." .format(r_server, radius_user)) return AccessReject # handle the RADIUS challenge if response.code == pyrad.packet.AccessChallenge: # now we map this to a privacyidea challenge if "State" in response: radius_state = response["State"][0] if "Reply-Message" in response: radius_message = response["Reply-Message"][0] result = AccessChallenge elif response.code == pyrad.packet.AccessAccept: radius_state = '<SUCCESS>' radius_message = 'RADIUS authentication succeeded' log.info(u"RADIUS server {0!s} granted " u"access to user {1!s}.".format( r_server, radius_user)) result = AccessAccept else: radius_state = '<REJECTED>' radius_message = 'RADIUS authentication failed' log.debug(u'radius response code {0!s}'.format(response.code)) log.info(u"Radiusserver {0!s} " u"rejected access to user {1!s}.".format( r_server, radius_user)) result = AccessReject except Exception as ex: # pragma: no cover log.error("Error contacting radius Server: {0!r}".format((ex))) log.info("{0!s}".format(traceback.format_exc())) options.update({'radius_result': result}) options.update({'radius_state': radius_state}) options.update({'radius_message': radius_message}) return result
def create_data_input(self, question, pin=None, pin_hash=None, counter=None, timesteps=None): """ Create the data_input to be used in the HMAC function In case of QN the question would be "111111" In case of QA the question would be "123ASD" In case of QH the question would be "BEEF" The question is transformed internally. :param question: The question can be :type question: str :param pin_hash: The hash of the pin :type pin_hash: basestring (hex) :param timesteps: timestemps :type timesteps: hex string :return: data_input :rtype: bytes """ # In case the ocrasuite comes as a unicode (like from the webui) we # need to convert it! data_input = to_bytes(self.ocrasuite) + b'\0' # Check for counter if self.ocrasuite_obj.counter == "C": if counter: counter = int(counter) counter = struct.pack('>Q', int(counter)) data_input += counter else: raise Exception( "The ocrasuite {0!s} requires a counter".format( self.ocrasuite)) # Check for Question if self.ocrasuite_obj.challenge_type == "QN": # question contains only numeric values hex_q = '{0:x}'.format(int(question)) hex_q += '0' * (len(hex_q) % 2) bin_q = binascii.unhexlify(hex_q) bin_q += b'\x00' * (128 - len(bin_q)) data_input += bin_q elif self.ocrasuite_obj.challenge_type == "QA": # question contains alphanumeric characters bin_q = to_bytes(question) bin_q += b'\x00' * (128 - len(bin_q)) data_input += bin_q elif self.ocrasuite_obj.challenge_type == "QH": # qustion contains hex values bin_q = binascii.unhexlify(question) bin_q += b'\x00' * (128 - len(bin_q)) data_input += bin_q # in case of PIN if self.ocrasuite_obj.signature_type == "P": if pin_hash: data_input += binascii.unhexlify(pin_hash) elif pin: pin_hash = SHA_FUNC.get(self.ocrasuite_obj.signature_hash)( to_bytes(pin)).digest() data_input += pin_hash else: raise Exception("The ocrasuite {0!s} requires a PIN!".format( self.ocrasuite)) elif self.ocrasuite_obj.signature_type == "T": if not timesteps: raise Exception( "The ocrasuite {0!s} requires timesteps".format( self.ocrasuite)) # In case of Time timesteps = int(timesteps, 16) timesteps = struct.pack('>Q', int(timesteps)) data_input += timesteps elif self.ocrasuite_obj.signature_type == "S": # pragma: no cover # In case of session # TODO: Session not yet implemented raise NotImplementedError("OCRA Session not implemented, yet.") return data_input
def check_otp(self, otpval, counter=None, window=None, options=None): """ run the RADIUS request against the RADIUS server :param otpval: the OTP value :param counter: The counter for counter based otp values :type counter: int :param window: a counter window :type counter: int :param options: additional token specific options :type options: dict :return: counter of the matching OTP value. :rtype: int """ otp_count = -1 options = options or {} radius_dictionary = None radius_identifier = self.get_tokeninfo("radius.identifier") radius_user = self.get_tokeninfo("radius.user") system_radius_settings = self.get_tokeninfo("radius.system_settings") if radius_identifier: # New configuration radius_server_object = get_radius(radius_identifier) radius_server = radius_server_object.config.server radius_port = radius_server_object.config.port radius_server = u"{0!s}:{1!s}".format(radius_server, radius_port) radius_secret = radius_server_object.get_secret() radius_dictionary = radius_server_object.config.dictionary elif system_radius_settings: # system configuration radius_server = get_from_config("radius.server") radius_secret = get_from_config("radius.secret") else: # individual token settings radius_server = self.get_tokeninfo("radius.server") # Read the secret secret = self.token.get_otpkey() radius_secret = binascii.unhexlify(secret.getKey()) # here we also need to check for radius.user log.debug(u"checking OTP len:{0!s} on radius server: " u"{1!s}, user: {2!r}".format(len(otpval), radius_server, radius_user)) try: # pyrad does not allow to set timeout and retries. # it defaults to retries=3, timeout=5 # TODO: At the moment we support only one radius server. # No round robin. server = radius_server.split(':') r_server = server[0] r_authport = 1812 if len(server) >= 2: r_authport = int(server[1]) nas_identifier = get_from_config("radius.nas_identifier", "privacyIDEA") if not radius_dictionary: radius_dictionary = get_from_config("radius.dictfile", "/etc/privacyidea/dictionary") log.debug(u"NAS Identifier: %r, " u"Dictionary: %r" % (nas_identifier, radius_dictionary)) log.debug(u"constructing client object " u"with server: %r, port: %r, secret: %r" % (r_server, r_authport, to_unicode(radius_secret))) srv = Client(server=r_server, authport=r_authport, secret=to_bytes(radius_secret), dict=Dictionary(radius_dictionary)) req = srv.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name=radius_user.encode('utf-8'), NAS_Identifier=nas_identifier.encode('ascii')) req["User-Password"] = req.PwCrypt(otpval) if "transactionid" in options: req["State"] = str(options.get("transactionid")) response = srv.SendPacket(req) # TODO: handle the RADIUS challenge """ if response.code == pyrad.packet.AccessChallenge: opt = {} for attr in response.keys(): opt[attr] = response[attr] res = False log.debug("challenge returned %r " % opt) # now we map this to a privacyidea challenge if "State" in opt: reply["transactionid"] = opt["State"][0] if "Reply-Message" in opt: reply["message"] = opt["Reply-Message"][0] """ if response.code == pyrad.packet.AccessAccept: log.info("Radiusserver %s granted " "access to user %s." % (r_server, radius_user)) otp_count = 0 else: log.warning("Radiusserver %s rejected " "access to user %s." % (r_server, radius_user)) except Exception as ex: # pragma: no cover log.error("Error contacting radius Server: {0!r}".format((ex))) log.debug("{0!s}".format(traceback.format_exc())) return otp_count
def create_data_input(self, question, pin=None, pin_hash=None, counter=None, timesteps=None): """ Create the data_input to be used in the HMAC function In case of QN the question would be "111111" In case of QA the question would be "123ASD" In case of QH the question would be "BEEF" The question is transformed internally. :param question: The question can be :type question: str :param pin_hash: The hash of the pin :type pin_hash: basestring (hex) :param timesteps: timestemps :type timesteps: hex string :return: data_input :rtype: bytes """ # In case the ocrasuite comes as a unicode (like from the webui) we # need to convert it! data_input = to_bytes(self.ocrasuite) + b'\0' # Check for counter if self.ocrasuite_obj.counter == "C": if counter: counter = int(counter) counter = struct.pack('>Q', int(counter)) data_input += counter else: raise Exception("The ocrasuite {0!s} requires a counter".format( self.ocrasuite)) # Check for Question if self.ocrasuite_obj.challenge_type == "QN": # question contains only numeric values hex_q = '{0:x}'.format(int(question)) hex_q += '0' * (len(hex_q) % 2) bin_q = binascii.unhexlify(hex_q) bin_q += b'\x00' * (128-len(bin_q)) data_input += bin_q elif self.ocrasuite_obj.challenge_type == "QA": # question contains alphanumeric characters bin_q = to_bytes(question) bin_q += b'\x00' * (128-len(bin_q)) data_input += bin_q elif self.ocrasuite_obj.challenge_type == "QH": # qustion contains hex values bin_q = binascii.unhexlify(question) bin_q += b'\x00' * (128-len(bin_q)) data_input += bin_q # in case of PIN if self.ocrasuite_obj.signature_type == "P": if pin_hash: data_input += binascii.unhexlify(pin_hash) elif pin: pin_hash = SHA_FUNC.get(self.ocrasuite_obj.signature_hash)( to_bytes(pin)).digest() data_input += pin_hash else: raise Exception("The ocrasuite {0!s} requires a PIN!".format( self.ocrasuite)) elif self.ocrasuite_obj.signature_type == "T": if not timesteps: raise Exception("The ocrasuite {0!s} requires timesteps".format( self.ocrasuite)) # In case of Time timesteps = int(timesteps, 16) timesteps = struct.pack('>Q', int(timesteps)) data_input += timesteps elif self.ocrasuite_obj.signature_type == "S": # pragma: no cover # In case of session # TODO: Session not yet implemented raise NotImplementedError("OCRA Session not implemented, yet.") return data_input
def test_01_create_token(self): db_token = Token(self.serial1, tokentype="push") db_token.save() token = PushTokenClass(db_token) self.assertEqual(token.token.serial, self.serial1) self.assertEqual(token.token.tokentype, "push") self.assertEqual(token.type, "push") class_prefix = token.get_class_prefix() self.assertEqual(class_prefix, "PIPU") self.assertEqual(token.get_class_type(), "push") # Test to do the 2nd step, although the token is not yet in clientwait self.assertRaises(ParameterError, token.update, { "otpkey": "1234", "pubkey": "1234", "serial": self.serial1 }) # Run enrollment step 1 token.update({"genkey": 1}) # Now the token is in the state clientwait, but insufficient parameters would still fail self.assertRaises(ParameterError, token.update, {"otpkey": "1234"}) self.assertRaises(ParameterError, token.update, { "otpkey": "1234", "pubkey": "1234" }) # Unknown config self.assertRaises(ParameterError, token.get_init_detail, params={"firebase_config": "bla"}) fb_config = { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.APP_ID: "2", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4" } # Wrong JSON file self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) # Wrong Project number fb_config[FIREBASE_CONFIG.JSON_CONFIG] = FIREBASE_FILE self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) # Missing APP_ID self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4" }) # Missing API_KEY_IOS self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.APP_ID_IOS: "1", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4" }) # Everything is fine fb_config[FIREBASE_CONFIG.PROJECT_ID] = "test-123456" r = set_smsgateway( "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) self.assertTrue(r > 0) detail = token.get_init_detail( params={"firebase_config": self.firebase_config_name}) self.assertEqual(detail.get("serial"), self.serial1) self.assertEqual(detail.get("rollout_state"), "clientwait") enrollment_credential = detail.get("enrollment_credential") self.assertTrue("pushurl" in detail) self.assertFalse("otpkey" in detail) # Run enrollment step 2 token.update({ "enrollment_credential": enrollment_credential, "serial": self.serial1, "fbtoken": "firebasetoken", "pubkey": self.smartphone_public_key_pem_urlsafe }) self.assertEqual(token.get_tokeninfo("firebase_token"), "firebasetoken") self.assertEqual(token.get_tokeninfo("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertTrue( token.get_tokeninfo("public_key_server").startswith( u"-----BEGIN RSA PUBLIC KEY-----\n"), token.get_tokeninfo("public_key_server")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(token.get_tokeninfo("public_key_server")), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) self.assertTrue( token.get_tokeninfo("private_key_server").startswith( u"-----BEGIN RSA PRIVATE KEY-----\n"), token.get_tokeninfo("private_key_server")) parsed_server_privkey = serialization.load_pem_private_key( to_bytes(token.get_tokeninfo("private_key_server")), None, default_backend()) self.assertIsInstance(parsed_server_privkey, RSAPrivateKey) detail = token.get_init_detail() self.assertEqual(detail.get("rollout_state"), "enrolled") augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_stripped_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertEqual(parsed_server_pubkey.public_numbers(), parsed_stripped_server_pubkey.public_numbers()) remove_token(self.serial1)
def test_02_api_enroll(self): self.authenticate() # Failed enrollment due to missing policy with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertNotEqual(res.status_code, 200) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "Missing enrollment policy for push token: push_firebase_configuration" ) self.assertEqual(error.get("code"), 303) r = set_smsgateway( self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.APP_ID: "2", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "test-123456", FIREBASE_CONFIG.JSON_CONFIG: FIREBASE_FILE }) self.assertTrue(r > 0) set_policy("push1", scope=SCOPE.ENROLL, action="{0!s}={1!s}".format(PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name)) # 1st step with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = res.json.get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertTrue("pushurl" in detail) # check that the new URL contains the serial number self.assertTrue( "&serial=PIPU" in detail.get("pushurl").get("value")) self.assertTrue("appid=" in detail.get("pushurl").get("value")) self.assertTrue("appidios=" in detail.get("pushurl").get("value")) self.assertTrue("apikeyios=" in detail.get("pushurl").get("value")) self.assertFalse("otpkey" in detail) enrollment_credential = detail.get("enrollment_credential") # 2nd step. Failing with wrong serial number with self.app.test_request_context( '/ttype/push', method='POST', data={ "serial": "wrongserial", "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 404, res) status = res.json.get("result").get("status") self.assertFalse(status) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "No token with this serial number in the rollout state 'clientwait'." ) # 2nd step. Fails with missing enrollment credential with self.app.test_request_context( '/ttype/push', method='POST', data={ "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT", "enrollment_credential": "WRonG" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 400, res) status = res.json.get("result").get("status") self.assertFalse(status) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "ERR905: Invalid enrollment credential. You are not authorized to finalize this token." ) # 2nd step: as performed by the smartphone with self.app.test_request_context( '/ttype/push', method='POST', data={ "enrollment_credential": enrollment_credential, "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) detail = res.json.get("detail") # still the same serial number self.assertEqual(serial, detail.get("serial")) self.assertEqual(detail.get("rollout_state"), "enrolled") # Now the smartphone gets a public key from the server augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) pubkey = detail.get("public_key") # Now check, what is in the token in the database toks = get_tokens(serial=serial) self.assertEqual(len(toks), 1) token_obj = toks[0] self.assertEqual(token_obj.token.rollout_state, u"enrolled") self.assertTrue(token_obj.token.active) tokeninfo = token_obj.get_tokeninfo() self.assertEqual(tokeninfo.get("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertEqual(tokeninfo.get("firebase_token"), u"firebaseT") self.assertEqual( tokeninfo.get("public_key_server").strip().strip( "-BEGIN END RSA PUBLIC KEY-").strip(), pubkey) # The token should also contain the firebase config self.assertEqual(tokeninfo.get(PUSH_ACTION.FIREBASE_CONFIG), self.firebase_config_name)
def compare(self, key): bhOtpKey = binascii.unhexlify(key) enc_otp_key = to_bytes(encrypt(bhOtpKey, self.iv)) return enc_otp_key == self.val
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. """ res = False options = options or {} message = get_action_values_from_options(SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT sslverify = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") attributes = None data = None challenge = b32encode_and_unicode(geturandom()) fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: # We send the challenge to the Firebase service fb_gateway = create_sms_instance(fb_identifier) url = fb_gateway.smsgateway.option_dict.get(FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = {"nonce": challenge, "question": message_on_mobile, "serial": self.token.serial, "title": title, "sslverify": sslverify, "url": url} # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(**smartphone_data) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) res = fb_gateway.submit_message(self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError("Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format(self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError("The token has no tokeninfo. Can not send via firebase service.") validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes
def test_email(config, recipient, subject, body, sender=None, reply_to=None, mimetype="plain"): """ Sends an email via the configuration. :param config: The email configuration :type config: dict :param recipient: The recipients of the email :type recipient: list :param subject: The subject of the email :type subject: basestring :param body: The body of the email :type body: basestring :param sender: An optional sender of the email. The SMTP database object has its own sender. This parameter can be used to override the internal sender. :type sender: basestring :param reply_to: The Reply-To parameter :type reply_to: basestring :param mimetype: The type of the email to send. Can by plain or html :return: True or False """ if type(recipient) != list: recipient = [recipient] mail_from = sender or config['sender'] reply_to = reply_to or mail_from msg = MIMEText(body.encode('utf-8'), mimetype, 'utf-8') msg['Subject'] = subject msg['From'] = mail_from msg['To'] = ",".join(recipient) msg['Date'] = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) msg['Reply-To'] = reply_to mail = smtplib.SMTP(config['server'], port=int(config['port']), timeout=config.get('timeout', TIMEOUT)) log.debug(u"submitting message to {0!s}".format(msg["To"])) log.debug("Saying EHLO to mailserver {0!s}".format(config['server'])) r = mail.ehlo() log.debug("mailserver responded with {0!s}".format(r)) # Start TLS if required if config.get('tls', False): log.debug("Trying to STARTTLS: {0!s}".format(config['tls'])) mail.starttls() # Authenticate, if a username is given. if config.get('username', ''): log.debug("Doing authentication with {0!s}".format( config['username'])) password = decryptPassword(config['password']) if password == FAILED_TO_DECRYPT_PASSWORD: password = config['password'] # Under Python 2, we pass passwords as bytestrings to get CRAM-MD5 to work. # We add a safeguard config option to disable the conversion. # Under Python 3, we pass passwords as unicode. if six.PY2 and get_app_config_value("PI_SMTP_PASSWORD_AS_BYTES", True): password = to_bytes(password) mail.login(config['username'], password) r = mail.sendmail(mail_from, recipient, msg.as_string()) log.info("Mail sent: {0!s}".format(r)) # r is a dictionary like {"*****@*****.**": (200, 'OK')} # we change this to True or False success = True for one_recipient in recipient: res_id, res_text = r.get(one_recipient, (200, "OK")) if res_id != 200 and res_text != "OK": success = False log.error("Failed to send email to {0!r}: {1!r}, {2!r}".format( one_recipient, res_id, res_text)) mail.quit() log.debug("I am done sending your email.") return success
def test_email(config, recipient, subject, body, sender=None, reply_to=None, mimetype="plain"): """ Sends an email via the configuration. :param config: The email configuration :type config: dict :param recipient: The recipients of the email :type recipient: list :param subject: The subject of the email :type subject: basestring :param body: The body of the email :type body: basestring :param sender: An optional sender of the email. The SMTP database object has its own sender. This parameter can be used to override the internal sender. :type sender: basestring :param reply_to: The Reply-To parameter :type reply_to: basestring :param mimetype: The type of the email to send. Can by plain or html :return: True or False """ if type(recipient) != list: recipient = [recipient] mail_from = sender or config['sender'] reply_to = reply_to or mail_from msg = MIMEText(body.encode('utf-8'), mimetype, 'utf-8') msg['Subject'] = subject msg['From'] = mail_from msg['To'] = ",".join(recipient) msg['Date'] = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) msg['Reply-To'] = reply_to mail = smtplib.SMTP(config['server'], port=int(config['port']), timeout=config.get('timeout', TIMEOUT)) log.debug(u"submitting message to {0!s}".format(msg["To"])) log.debug("Saying EHLO to mailserver {0!s}".format(config['server'])) r = mail.ehlo() log.debug("mailserver responded with {0!s}".format(r)) # Start TLS if required if config.get('tls', False): log.debug("Trying to STARTTLS: {0!s}".format(config['tls'])) mail.starttls() # Authenticate, if a username is given. if config.get('username', ''): log.debug("Doing authentication with {0!s}".format(config['username'])) password = decryptPassword(config['password']) if password == FAILED_TO_DECRYPT_PASSWORD: password = config['password'] # Under Python 2, we pass passwords as bytestrings to get CRAM-MD5 to work. # We add a safeguard config option to disable the conversion. # Under Python 3, we pass passwords as unicode. if six.PY2 and get_app_config_value("PI_SMTP_PASSWORD_AS_BYTES", True): password = to_bytes(password) mail.login(config['username'], password) r = mail.sendmail(mail_from, recipient, msg.as_string()) log.info("Mail sent: {0!s}".format(r)) # r is a dictionary like {"*****@*****.**": (200, 'OK')} # we change this to True or False success = True for one_recipient in recipient: res_id, res_text = r.get(one_recipient, (200, "OK")) if res_id != 200 and res_text != "OK": success = False log.error("Failed to send email to {0!r}: {1!r}, {2!r}".format(one_recipient, res_id, res_text)) mail.quit() log.debug("I am done sending your email.") return success
def api_endpoint(cls, request, g): """ This provides a function which is called by the API endpoint /ttype/push which is defined in api/ttype.py The method returns return "json", {} This endpoint is used for the 2nd enrollment step of the smartphone. Parameters sent: * serial * fbtoken * pubkey This endpoint is also used, if the smartphone sends the signed response to the challenge during authentication Parameters sent: * serial * nonce (which is the challenge) * signature (which is the signed nonce) :param request: The Flask request :param g: The Flask global object g :return: dictionary """ details = {} result = False serial = getParam(request.all_data, "serial", optional=False) if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data: log.debug("Do the 2nd step of the enrollment.") try: token_obj = get_one_token(serial=serial, tokentype="push", rollout_state="clientwait") token_obj.update(request.all_data) except ResourceNotFoundError: raise ResourceNotFoundError("No token with this serial number in the rollout state 'clientwait'.") init_detail_dict = request.all_data details = token_obj.get_init_detail(init_detail_dict) result = True elif serial and "nonce" in request.all_data and "signature" in request.all_data: log.debug("Handling the authentication response from the smartphone.") challenge = getParam(request.all_data, "nonce") serial = getParam(request.all_data, "serial") signature = getParam(request.all_data, "signature") # get the token_obj for the given serial: token_obj = get_one_token(serial=serial, tokentype="push") pubkey_pem = token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE) # The public key of the smartphone was probably sent as urlsafe: pubkey_pem = pubkey_pem.replace("-", "+").replace("_", "/") # The public key was sent without any header pubkey_pem = "-----BEGIN PUBLIC KEY-----\n{0!s}\n-----END PUBLIC KEY-----".format(pubkey_pem.strip().replace(" ", "+")) # Do the 2nd step of the authentication # Find valid challenges challengeobject_list = get_challenges(serial=serial, challenge=challenge) if challengeobject_list: # There are valid challenges, so we check this signature for chal in challengeobject_list: # verify the signature of the nonce pubkey_obj = serialization.load_pem_public_key(to_bytes(pubkey_pem), default_backend()) sign_data = u"{0!s}|{1!s}".format(challenge, serial) try: pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid log.debug("Found matching challenge {0!s}.".format(chal)) chal.set_otp_status(True) chal.save() result = True except InvalidSignature as e: pass else: raise ParameterError("Missing parameters!") return "json", prepare_result(result, details=details)