def process_compute_request(data, user_nonce: UserNonce): required_attributes = ['signature', 'consumerAddress'] msg, status = check_required_attributes(required_attributes, data, 'compute') if msg: raise BadRequestError(msg) provider_wallet = get_provider_wallet() did = data.get('documentId') owner = data.get('consumerAddress') job_id = data.get('jobId') body = dict() body['providerAddress'] = provider_wallet.address if owner is not None: body['owner'] = owner if job_id is not None: body['jobId'] = job_id if did is not None: body['documentId'] = did # Consumer signature signature = data.get('signature') original_msg = f'{body.get("owner", "")}{body.get("jobId", "")}{body.get("documentId", "")}' verify_signature(owner, signature, original_msg, user_nonce.get_nonce(owner)) msg_to_sign = f'{provider_wallet.address}{body.get("jobId", "")}{body.get("documentId", "")}' msg_hash = add_ethereum_prefix_and_hash_msg(msg_to_sign) body['providerSignature'] = Web3Helper.sign_hash(msg_hash, provider_wallet) return body
def validate_signature(self, value, params, **kwargs): """ Validates a signature using the documentId, jobId and consumerAddress. parameters: - name: value type: string description: Value of the field being validated - name: params type: list description: The list of parameters defined for the rule, i.e. names of other fields inside the request. """ self._assert_params_size(size=3, params=params, rule="signature") owner = self._attribute_value(params[0]) or "" did = self._attribute_value(params[1]) or "" job_id = self._attribute_value(params[2]) or "" original_msg = f"{owner}{job_id}{did}" try: verify_signature(owner, value, original_msg, get_nonce(owner)) return True except InvalidSignatureError: return False return False
def test_auth_token(): token = "0x1d2741dee30e64989ef0203957c01b14f250f5d2f6ccb0" \ "c88c9518816e4fcec16f84e545094eb3f377b7e214ded226" \ "76fbde8ca2e41b4eb1b3565047ecd9acf300-1568372035" pub_address = "0xe2DD09d719Da89e5a3D0F2549c7E24566e947260" doc_id = "663516d306904651bbcf9fe45a00477c215c7303d8a24c5bad6005dd2f95e68e" assert is_auth_token_valid(token), f'cannot recognize auth-token {token}' address = check_auth_token(token) assert address and address.lower() == pub_address.lower(), f'address mismatch, got {address}, ' \ f'' \ f'' \ f'expected {pub_address}' try: verify_signature(pub_address, token, doc_id) except InvalidSignatureError as e: assert False, f'invalid signature/auth-token {token}, {pub_address}, {doc_id}: {e}'
def process_consume_request(data: dict, method: str, user_nonce: UserNonce = None, additional_params: list = None, require_signature: bool = True): required_attributes = [ 'documentId', 'serviceId', 'serviceType', 'dataToken', 'consumerAddress' ] if additional_params: required_attributes += additional_params if require_signature: required_attributes.append('signature') msg, status = check_required_attributes(required_attributes, data, method) if msg: raise AssertionError(msg) did = data.get('documentId') token_address = data.get('dataToken') consumer_address = data.get('consumerAddress') service_id = data.get('serviceId') service_type = data.get('serviceType') # grab asset for did from the metadatastore associated with the Data Token address asset = get_asset_from_metadatastore(get_metadata_url(), did) service = ServiceAgreement.from_ddo(service_type, asset) if service.type != service_type: raise AssertionError( f'Requested service with id {service_id} has type {service.type} which ' f'does not match the requested service type {service_type}.') if require_signature: assert user_nonce, '`user_nonce` is required when signature is required.' # Raises ValueError when signature is invalid signature = data.get('signature') verify_signature(consumer_address, signature, did, user_nonce.get_nonce(consumer_address)) return asset, service, did, consumer_address, token_address
def process_consume_request(data, method, additional_params=None, require_signature=True): required_attributes = [ 'documentId', 'serviceId', 'serviceType', 'tokenAddress', 'consumerAddress' ] if additional_params: required_attributes += additional_params if require_signature: required_attributes.append('signature') msg, status = check_required_attributes(required_attributes, data, method) if msg: raise AssertionError(msg) did = data.get('documentId') token_address = data.get('tokenAddress') consumer_address = data.get('consumerAddress') service_id = data.get('serviceId') service_type = data.get('serviceType') # grab asset for did from the metadatastore associated with the Data Token address asset = get_asset_for_data_token(token_address, did) service = asset.get_service_by_index(service_id) if service.type != service_type: raise AssertionError( f'Requested service with id {service_id} has type {service.type} which ' f'does not match the requested service type {service_type}.') if require_signature: # Raises ValueError when signature is invalid signature = data.get('signature') verify_signature(consumer_address, signature, did) return asset, service, did, consumer_address, token_address
def computeStart(): """Call the execution of a workflow. --- tags: - services consumes: - application/json parameters: - name: signature in: query description: Signature of (consumerAddress+jobId+documentId) to verify the consumer of this asset/compute job. The signature uses ethereum based signing method (see https://github.com/ethereum/EIPs/pull/683) type: string - name: consumerAddress in: query description: The consumer ethereum address. required: true type: string - name: algorithmDid in: query description: The DID of the algorithm Asset to be executed required: false type: string - name: algorithmMeta in: query description: json object that define the algorithm attributes and url or raw code required: false type: json string - name: output in: query description: json object that define the output section required: true type: json string responses: 200: description: Call to the operator-service was successful. 400: description: One of the required attributes is missing. 401: description: Consumer signature is invalid or failed verification 500: description: General server error """ data = get_request_data(request) try: asset, service, did, consumer_address, token_address = process_consume_request( # noqa data, 'compute_start_job', additional_params=["transferTxId", "output"], require_signature=False) service_id = data.get('serviceId') service_type = data.get('serviceType') signature = data.get('signature') tx_id = data.get("transferTxId") # Verify that the number of required tokens has been # transferred to the provider's wallet. _tx, _order_log, _transfer_log = validate_order( consumer_address, token_address, float(service.get_cost()), tx_id, add_0x_prefix(did_to_id(did)) if did.startswith('did:') else did, service_id) validate_transfer_not_used_for_other_service(did, service_id, tx_id, consumer_address, token_address) record_consume_request(did, service_id, tx_id, consumer_address, token_address, service.get_cost()) algorithm_did = data.get('algorithmDid') algorithm_token_address = data.get('algorithmDataToken') algorithm_meta = data.get('algorithmMeta') algorithm_tx_id = data.get('algorithmTransferTxId') output_def = data.get('output', dict()) assert service_type == ServiceTypes.CLOUD_COMPUTE # Validate algorithm choice if not (algorithm_meta or algorithm_did): msg = f'Need an `algorithmMeta` or `algorithmDid` to run, otherwise don\'t bother.' # noqa logger.error(msg, exc_info=1) return jsonify(error=msg), 400 # algorithmDid also requires algorithmDataToken # and algorithmTransferTxId if algorithm_did: if not (algorithm_token_address and algorithm_tx_id): msg = ( f'Using `algorithmDid` requires the `algorithmDataToken` and ' # noqa f'`algorithmTransferTxId` values in the request payload. ' f'algorithmDataToken is the DataToken address for the algorithm asset. ' # noqa f'algorithmTransferTxId is the transaction id (hash) of transferring ' # noqa f'data tokens from consumer wallet to this providers wallet.' ) logger.error(msg, exc_info=1) return jsonify(error=msg), 400 # Consumer signature original_msg = f'{consumer_address}{did}' verify_signature(consumer_address, signature, original_msg, user_nonce.get_nonce(consumer_address)) ######################## # Valid service? if service is None: return jsonify( error=f'This DID has no compute service {did}.'), 400 ######################### # Check privacy privacy_options = service.main.get('privacy', {}) if (algorithm_meta and privacy_options.get('allowRawAlgorithm', True) is False): return jsonify( error=f'cannot run raw algorithm on this did {did}.'), 400 trusted_algorithms = privacy_options.get('trustedAlgorithms', []) if (algorithm_did and trusted_algorithms and algorithm_did not in trusted_algorithms): return jsonify( error=f'cannot run raw algorithm on this did {did}.'), 400 ######################### # Validate ALGORITHM meta if algorithm_meta: algorithm_meta = json.loads(algorithm_meta) if isinstance( algorithm_meta, str) else algorithm_meta algorithm_dict = build_stage_algorithm_dict( consumer_address, algorithm_did, algorithm_token_address, algorithm_tx_id, algorithm_meta, provider_wallet) error_msg, status_code = validate_algorithm_dict( algorithm_dict, algorithm_did) if error_msg: return jsonify(error=error_msg), status_code ######################### # INPUT asset_urls = get_asset_download_urls( asset, provider_wallet, config_file=app.config['CONFIG_FILE']) if not asset_urls: return jsonify(error=f'cannot get url(s) in input did {did}.'), 400 input_dict = dict({'index': 0, 'id': did, 'url': asset_urls}) ######################### # OUTPUT if output_def: output_def = json.loads(output_def) if isinstance( output_def, str) else output_def output_dict = build_stage_output_dict(output_def, asset, consumer_address, provider_wallet) ######################### # STAGE stage = build_stage_dict(input_dict, algorithm_dict, output_dict) ######################### # WORKFLOW workflow = dict({'stages': list([stage])}) # workflow is ready, push it to operator logger.info('Sending: %s', workflow) msg_to_sign = f'{provider_wallet.address}{did}' msg_hash = add_ethereum_prefix_and_hash_msg(msg_to_sign) payload = { 'workflow': workflow, 'providerSignature': Web3Helper.sign_hash(msg_hash, provider_wallet), 'documentId': did, 'agreementId': tx_id, 'owner': consumer_address, 'providerAddress': provider_wallet.address } response = requests_session.post( get_compute_endpoint(), data=json.dumps(payload), headers={'content-type': 'application/json'}) user_nonce.increment_nonce(consumer_address) return Response(response.content, response.status_code, headers={'content-type': 'application/json'}) except InvalidSignatureError as e: msg = f'Consumer signature failed verification: {e}' logger.error(msg, exc_info=1) return jsonify(error=msg), 401 except (ValueError, KeyError, Exception) as e: logger.error(f'Error- {str(e)}', exc_info=1) return jsonify(error=f'Error : {str(e)}'), 500
def encrypt(): """Encrypt document using the Provider's own symmetric key (symmetric encryption). This can be used by the publisher of an asset to encrypt the urls of the asset data files before publishing the asset ddo. The publisher to use this service is one that is using a front-end with a wallet app such as MetaMask. The `urls` are encrypted by the provider so that the provider will be able to decrypt at time of providing the service later on. tags: - services consumes: - application/json parameters: - in: body name: body required: true description: Asset urls encryption. schema: type: object required: - documentId - signature - document - publisherAddress: properties: documentId: description: Identifier of the asset to be registered in ocean. type: string example: 'did:op:08a429b8529856d59867503f8056903a680935a76950bb9649785cc97869a43d' signature: description: Publisher signature of the documentId type: string example: '' document: description: document type: string example: '/some-url' publisherAddress: description: Publisher address. type: string example: '0x00a329c0648769A73afAc7F9381E08FB43dBEA72' responses: 201: description: document successfully encrypted. 500: description: Error return: the encrypted document (hex str) """ required_attributes = [ 'documentId', 'signature', 'document', 'publisherAddress' ] data = get_request_data(request) msg, status = check_required_attributes(required_attributes, data, 'encrypt') if msg: return msg, status did = data.get('documentId') signature = data.get('signature') document = json.dumps(json.loads(data.get('document')), separators=(',', ':')) publisher_address = data.get('publisherAddress') try: # Raises ValueError when signature is invalid verify_signature(publisher_address, signature, did) encrypted_document = do_encrypt( document, provider_acc, ) logger.info(f'encrypted urls {encrypted_document}, ' f'publisher {publisher_address}, ' f'documentId {did}') return Response(json.dumps({'encryptedDocument': encrypted_document}), 201, headers={'content-type': 'application/json'}) except InvalidSignatureError as e: msg = f'Publisher signature failed verification: {e}' logger.error(msg, exc_info=1) return jsonify(error=msg), 401 except Exception as e: logger.error( f'Error: {e}. \n' f'providerAddress={provider_acc.address}\n' f'Payload was: documentId={did}, ' f'publisherAddress={publisher_address},' f'signature={signature}', exc_info=1) return jsonify(error=e), 500
def computeStatus(): """Get status for a specific jobId/documentId/owner --- tags: - services consumes: - application/json parameters: - name: signature in: query description: Signature of (consumerAddress+jobId+documentId) to verify the consumer of this asset/compute job. The signature uses ethereum based signing method (see https://github.com/ethereum/EIPs/pull/683) type: string - name: documentId in: query description: The ID of the asset. If not provided, the status of all currently running and old compute jobs for the specified consumerAddress will be returned. required: true type: string - name: consumerAddress in: query description: The consumer ethereum address. required: true type: string - name: jobId in: query description: The ID of the compute job. If not provided, all running compute jobs of the specified consumerAddress/documentId are suspended type: string responses: 200: description: Call to the operator-service was successful. 400: description: One of the required attributes is missing. 401: description: Consumer signature is invalid or failed verification. 500: description: General server error """ data = get_request_data(request) try: body = process_compute_request(data) response = requests_session.get( get_compute_endpoint(), params=body, headers={"content-type": "application/json"}, ) _response = response.content signed_request = bool(data.get("signature")) if signed_request: owner = data.get("consumerAddress") did = data.get("documentId") jobId = data.get("jobId") original_msg = f"{owner}{jobId}{did}" try: verify_signature( owner, data.get("signature"), original_msg, get_nonce(owner) ) except InvalidSignatureError: signed_request = False increment_nonce(owner) # Filter status info if signature is not given or failed validation if not signed_request: resp_content = json.loads(response.content.decode("utf-8")) if not isinstance(resp_content, list): resp_content = [resp_content] _response = [] keys_to_filter = ["resultsUrl", "algorithmLogUrl", "resultsDid", "owner"] for job_info in resp_content: for k in keys_to_filter: job_info.pop(k) _response.append(job_info) _response = json.dumps(_response) return Response( _response, response.status_code, headers={"content-type": "application/json"}, ) except (ValueError, Exception) as e: logger.error(f"Error- {str(e)}", exc_info=1) return jsonify(error=f"Error : {str(e)}"), 500