def test_is_compressed_pk(): wrong_values = [ None, 3, 15.23, "", {}, (), object, str, get_random_value_hex(32), get_random_value_hex(34), "06" + get_random_value_hex(32), ] # check_user_pk must only accept values that is not a 33-byte hex string for i in range(100): if i % 2: prefix = "02" else: prefix = "03" assert is_compressed_pk(prefix + get_random_value_hex(32)) # check_user_pk must only accept values that is not a 33-byte hex string for value in wrong_values: assert not is_compressed_pk(value)
def create_registration_receipt(user_id, available_slots, subscription_expiry): """ Creates a registration receipt. The receipt has the following format: ``user_id (33-byte) | available_slots (4-byte) | subscription_expiry (4-byte)`` All values are big endian. Args: user_id(:obj:`str`): the public key that identifies the user (33-bytes hex str). available_slots (:obj:`int`): the number of slots assigned to a user subscription (4-byte unsigned int). subscription_expiry (:obj:`int`): the expiry assigned to a user subscription (4-byte unsigned int). Returns: :obj:`bytes`: The serialized data to be signed. """ if not is_compressed_pk(user_id): raise InvalidParameter( "Provided public key does not match expected format (33-byte hex string)" ) elif not is_u4int(available_slots): raise InvalidParameter( "Provided available_slots must be a 4-byte unsigned integer") elif not is_u4int(subscription_expiry): raise InvalidParameter( "Provided subscription_expiry must be a 4-byte unsigned integer") return bytes.fromhex(user_id) + available_slots.to_bytes( 4, "big") + subscription_expiry.to_bytes(4, "big")
def add_update_user(self, user_id): """ Adds a new user or updates the subscription of an existing one, by adding additional slots. Args: user_id(:obj:`str`): the public key that identifies the user (33-bytes hex str). Returns: :obj:`tuple`: a tuple with the number of available slots in the user subscription and the subscription expiry (in absolute block height). Raises: :obj:`InvalidParameter`: if the user_pk does not match the expected format. """ if not is_compressed_pk(user_id): raise InvalidParameter("Provided public key does not match expected format (33-byte hex string)") if user_id not in self.registered_users: self.registered_users[user_id] = UserInfo( self.subscription_slots, self.block_processor.get_block_count() + self.subscription_duration ) else: # FIXME: For now new calls to register add subscription_slots to the current count and reset the expiry time self.registered_users[user_id].available_slots += self.subscription_slots self.registered_users[user_id].subscription_expiry = ( self.block_processor.get_block_count() + self.subscription_duration ) self.user_db.store_user(user_id, self.registered_users[user_id].to_dict()) return self.registered_users[user_id].available_slots, self.registered_users[user_id].subscription_expiry
def register(user_id, teos_url): """ Registers the user to the tower. Args: user_id (:obj:`str`): a 33-byte hex-encoded compressed public key representing the user. teos_url (:obj:`str`): the teos base url. Returns: :obj:`dict`: a dictionary containing the tower response if the registration succeeded. Raises: :obj:`InvalidParameter <cli.exceptions.InvalidParameter>`: if `user_id` is invalid. :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`TowerResponseError <cli.exceptions.TowerResponseError>`: if the tower responded with an error, or the response was invalid. """ if not is_compressed_pk(user_id): raise InvalidParameter("The cli public key is not valid") # Send request to the server. register_endpoint = "{}/register".format(teos_url) data = {"public_key": user_id} logger.info("Registering in the Eye of Satoshi") response = process_post_response(post_request(data, register_endpoint)) return response
def store_tower_record(self, tower_id, tower_data): """ Stores a tower record to the database. ``tower_id`` is used as identifier. Args: tower_id (:obj:`str`): a 33-byte hex-encoded string identifying the tower. tower_data (:obj:`dict`): the tower associated data, as a dictionary. Returns: :obj:`bool`: True if the tower record was stored in the database, False otherwise. """ if is_compressed_pk(tower_id): try: self.create_entry(tower_id, json.dumps(tower_data.to_dict())) self.plugin.log(f"Adding tower to Tower's db (id={tower_id})") return True except (json.JSONDecodeError, TypeError): self.plugin.log( f"Could't add tower to db. Wrong tower data format (tower_id={tower_id}, tower_data={tower_data.to_dict()})" ) return False else: self.plugin.log( f"Could't add user to db. Wrong pk format (tower_id={tower_id}, tower_data={tower_data.to_dict()})" ) return False
def store_user(self, user_id, user_data): """ Stores a user record to the database. ``user_pk`` is used as identifier. Args: user_id (:obj:`str`): a 33-byte hex-encoded string identifying the user. user_data (:obj:`dict`): the user associated data, as a dictionary. Returns: :obj:`bool`: True if the user was stored in the database, False otherwise. """ if is_compressed_pk(user_id): try: self.create_entry(user_id, json.dumps(user_data)) logger.info("Adding user to Gatekeeper's db", user_id=user_id) return True except json.JSONDecodeError: logger.info("Could't add user to db. Wrong user data format", user_id=user_id, user_data=user_data) return False except TypeError: logger.info("Could't add user to db", user_id=user_id, user_data=user_data) return False else: logger.info("Could't add user to db. Wrong pk format", user_id=user_id, user_data=user_data) return False
def parse_register_arguments(tower_id, host, port, config): """ Parses the arguments of the register command and checks that they are correct. Args: tower_id (:obj:`str`): the identifier of the tower to connect to (a compressed public key). host (:obj:`str`): the ip or hostname to connect to, optional. host (:obj:`int`): the port to connect to, optional. config: (:obj:`dict`): the configuration dictionary. Returns: :obj:`tuple`: the tower id and tower network address. Raises: :obj:`common.exceptions.InvalidParameter`: if any of the parameters is wrong or missing. """ if not isinstance(tower_id, str): raise InvalidParameter( f"tower id must be a compressed public key (33-byte hex value) not {str(tower_id)}" ) # tower_id is of the form tower_id@[ip][:][port] if "@" in tower_id: if not (host and port): tower_id, tower_netaddr = tower_id.split("@") if not tower_netaddr: raise InvalidParameter("no tower endpoint was provided") # Only host was specified or colons where specified but not port if ":" not in tower_netaddr: tower_netaddr = f"{tower_netaddr}:{config.get('DEFAULT_PORT')}" elif tower_netaddr.endswith(":"): tower_netaddr = f"{tower_netaddr}{config.get('DEFAULT_PORT')}" else: raise InvalidParameter( "cannot specify host as both xxx@yyy and separate arguments") # host was specified, but no port, defaulting elif host: tower_netaddr = f"{host}:{config.get('DEFAULT_PORT')}" # host and port specified elif host and port: tower_netaddr = f"{host}:{port}" else: raise InvalidParameter("tower host is missing") if not is_compressed_pk(tower_id): raise InvalidParameter( "tower id must be a compressed public key (33-byte hex value)") return tower_id, tower_netaddr
def get_user(self, user_id): """ Gets information about a specific user. Args: user_id (:obj:`str`): the id of the requested user. Raises: :obj:`InvalidParameter`: if `user_id` is not in the valid format. """ if not is_compressed_pk(user_id): raise InvalidParameter("Invalid user id") result = self.stub.get_user(GetUserRequest(user_id=user_id)) return result.user
def register(user_id, teos_id, teos_url): """ Registers the user to the tower. Args: user_id (:obj:`str`): a 33-byte hex-encoded compressed public key representing the user. teos_id (:obj:`str`): the tower's compressed public key. teos_url (:obj:`str`): the teos base url. Returns: :obj:`tuple`: A tuple containing the available slots count and the subscription expiry. Raises: :obj:`InvalidParameter`: if `user_id` is invalid. :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`TowerResponseError`: if the tower responded with an error, or the response was invalid. """ if not is_compressed_pk(user_id): raise InvalidParameter("The cli public key is not valid") # Send request to the server. register_endpoint = "{}/register".format(teos_url) data = {"public_key": user_id} logger.info("Registering in the Eye of Satoshi") response = process_post_response(post_request(data, register_endpoint)) available_slots = response.get("available_slots") subscription_expiry = response.get("subscription_expiry") tower_signature = response.get("subscription_signature") # Check that the server signed the response as it should. if not tower_signature: raise TowerResponseError( "The response does not contain the signature of the subscription") # Check that the signature is correct. subscription_receipt = receipts.create_registration_receipt( user_id, available_slots, subscription_expiry) rpk = Cryptographer.recover_pk(subscription_receipt, tower_signature) if teos_id != Cryptographer.get_compressed_pk(rpk): raise TowerResponseError( "The returned appointment's signature is invalid") return available_slots, subscription_expiry
def add_update_user(self, user_id): """ Adds a new user or updates the subscription of an existing one, by adding additional slots. Args: user_id(:obj:`str`): the public key that identifies the user (33-bytes hex str). Returns: :obj:`tuple`: A tuple with the number of available slots in the user subscription, the subscription expiry (in absolute block height), and the registration_receipt. Raises: :obj:`InvalidParameter`: if the user_id does not match the expected format. :obj:`ConnectionRefusedError`: if bitcoind cannot be reached. """ if not is_compressed_pk(user_id): raise InvalidParameter("Provided public key does not match expected format (33-byte hex string)") with self.rw_lock.gen_wlock(): block_count = self.block_processor.get_block_count() if user_id not in self.registered_users: self.registered_users[user_id] = UserInfo( self.subscription_slots, block_count + self.subscription_duration, ) else: # FIXME: For now new calls to register add subscription_slots to the current count and reset the expiry # time if not is_u4int(self.registered_users[user_id].available_slots + self.subscription_slots): raise InvalidParameter("Maximum slots reached for the subscription") self.registered_users[user_id].available_slots += self.subscription_slots self.registered_users[user_id].subscription_expiry = block_count + self.subscription_duration self.user_db.store_user(user_id, self.registered_users[user_id].to_dict()) receipt = create_registration_receipt( user_id, self.registered_users[user_id].available_slots, self.registered_users[user_id].subscription_expiry, ) return ( self.registered_users[user_id].available_slots, self.registered_users[user_id].subscription_expiry, receipt, )
def parse_get_appointment_arguments(tower_id, locator): """ Parses the arguments of the get_appointment command and checks that they are correct. Args: tower_id (:obj:`str`): the identifier of the tower to connect to (a compressed public key). locator (:obj:`str`): the locator of the appointment to query the tower about. Returns: :obj:`tuple`: the tower id and appointment locator. Raises: :obj:`common.exceptions.InvalidParameter`: if any of the parameters is wrong or missing. """ if not is_compressed_pk(tower_id): raise InvalidParameter( "tower id must be a compressed public key (33-byte hex value)") if not is_locator(locator): raise InvalidParameter("The provided locator is not valid", locator=locator) return tower_id, locator
def main(command, args, config): setup_data_folder(config.get("DATA_DIR")) # Set the teos url teos_url = "{}:{}".format(config.get("API_CONNECT"), config.get("API_PORT")) # If an http or https prefix if found, leaves the server as is. Otherwise defaults to http. if not teos_url.startswith("http"): teos_url = "http://" + teos_url try: if os.path.exists(config.get("USER_PRIVATE_KEY")): logger.debug("Client id found. Loading keys") user_sk, user_id = load_keys(config.get("USER_PRIVATE_KEY")) else: logger.info("Client id not found. Generating new keys") user_sk = Cryptographer.generate_key() Cryptographer.save_key_file(user_sk.to_der(), "user_sk", config.get("DATA_DIR")) user_id = Cryptographer.get_compressed_pk(user_sk.public_key) if command == "register": if not args: raise InvalidParameter( "Cannot register. No tower id was given") else: teos_id = args.pop(0) if not is_compressed_pk(teos_id): raise InvalidParameter( "Cannot register. Tower id has invalid format") available_slots, subscription_expiry = register( user_id, teos_id, teos_url) logger.info( "Registration succeeded. Available slots: {}".format( available_slots)) logger.info("Subscription expires at block {}".format( subscription_expiry)) teos_id_file = os.path.join(config.get("DATA_DIR"), "teos_pk") Cryptographer.save_key_file(bytes.fromhex(teos_id), teos_id_file, config.get("DATA_DIR")) if command == "add_appointment": teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY")) appointment_data = parse_add_appointment_args(args) appointment = create_appointment(appointment_data) start_block, signature = add_appointment(appointment, user_sk, teos_id, teos_url) save_appointment_receipt(appointment.to_dict(), start_block, signature, config.get("APPOINTMENTS_FOLDER_NAME")) elif command == "get_appointment": if not args: logger.error("No arguments were given") else: arg_opt = args.pop(0) if arg_opt in ["-h", "--help"]: sys.exit(help_get_appointment()) teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY")) appointment_data = get_appointment(arg_opt, user_sk, teos_id, teos_url) if appointment_data: logger.info(json.dumps(appointment_data, indent=4)) elif command == "get_subscription_info": if args: arg_opt = args.pop(0) if arg_opt in ["-h", "--help"]: sys.exit(help_get_subscription_info()) teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY")) subscription_info = get_subscription_info(user_sk, teos_id, teos_url) if subscription_info: logger.info(json.dumps(subscription_info, indent=4)) elif command == "help": if args: command = args.pop(0) if command == "register": sys.exit(help_register()) if command == "add_appointment": sys.exit(help_add_appointment()) if command == "get_subscription_info": sys.exit(help_get_subscription_info()) elif command == "get_appointment": sys.exit(help_get_appointment()) else: logger.error( "Unknown command. Use help to check the list of available commands" ) else: sys.exit(show_usage()) except ( FileNotFoundError, IOError, ConnectionError, ValueError, BasicException, ) as e: logger.error(str(e)) except Exception as e: logger.error("Unknown error occurred", error=str(e))
def test_load_teos_id(keyfiles): # Test that it correctly returns the teos id assert is_compressed_pk( teos_client.load_teos_id(keyfiles.public_key_file_path))