class PdoClientConnectHelper(PdoRegistryHelper): def __init__(self, url, keyfile=None, key_str=None, auto_generate=False, cli=False): super(PdoClientConnectHelper, self).__init__(url) if not cli: self._dbg_dump = PdoDbgDump(LOGGER) else: self._dbg_dump = PdoDbgDump() self._make_signer(keyfile, key_str, auto_generate) def generate_new_signer_key(self): self._make_signer(auto_generate=True) def get_signer_public_key_as_hex(self): return self._signer.get_public_key_as_hex() def get_signer_private_key_as_hex(self): return self._signer.get_private_key_as_hex() def _make_signer(self, keyfile=None, key_str=None, auto_generate=False): if key_str or auto_generate: self._signer = CreatePdoSawtoothSigner(key_str) elif keyfile: try: with open(keyfile) as fd: key_str = fd.read().strip() fd.close() except OSError as err: raise ClientConnectException( 'Failed to read private key: {}'.format(str(err))) self._signer = CreatePdoSawtoothSigner(key_str) else: raise ClientConnectException('No option to create a signing key') def get_status(self, batch_id, wait): try: result = self.send_request( 'batch_statuses?id={}&wait={}'.format(batch_id, wait), ) return yaml.safe_load(result)['data'][0]['status'] except BaseException as err: raise ClientConnectException(err) def send_transaction(self, payload, family, wait=None, transaction_output_list=None, transaction_input_list=None, verbose=False, exception_type=TimeoutError, transaction_dependency_list=None): if not transaction_output_list: if family == self.get_ccl_family_name(): transaction_output_list = [ self.get_ccl_info_prefix(), self.get_ccl_state_prefix() ] elif family == self.get_contract_registry_family_name(): transaction_output_list = [self.get_contract_prefix()] else: transaction_output_list = [self.get_enclave_prefix()] if not transaction_input_list: transaction_input_list = [ self.get_enclave_prefix(), self.get_contract_prefix(), '000000', self.get_ccl_info_prefix(), self.get_ccl_state_prefix() ] if verbose: self._dbg_dump.dump_str( "output_list: {}".format(transaction_output_list)) self._dbg_dump.dump_str( "intput_list: {}".format(transaction_input_list)) self._dbg_dump.dump_str("family: {}".format(family)) self._dbg_dump.dump_str( "dependency_list: {}".format(transaction_dependency_list)) header = TransactionHeader( signer_public_key=self.get_signer_public_key_as_hex(), family_name=family, family_version="1.0", inputs=transaction_input_list, outputs=transaction_output_list, dependencies=transaction_dependency_list, payload_sha512=self._sha512(payload), batcher_public_key=self.get_signer_public_key_as_hex(), nonce=time.time().hex().encode()).SerializeToString() signature = self._signer.sign(header) transaction = Transaction(header=header, payload=payload, header_signature=signature) batch_list = self._create_batch_list([transaction]) batch_id = batch_list.batches[0].header_signature if wait and wait > 0: wait_time = 0 start_time = time.time() response = self.send_request( "batches", batch_list.SerializeToString(), 'application/octet-stream', ) while wait_time < wait: status = self.get_status( batch_id, wait - int(wait_time), ) wait_time = time.time() - start_time if status != 'PENDING': if verbose: self._dbg_dump.dump_str( "Transaction status: '{}'".format(status)) if status != "COMMITTED" and exception_type: # this is a temporary fix for the fact that Sawtooth may return INVALID status for a short while after submitting batch for commit # FIX: if total wait time < 10 (ad hoc), and we get INVALID, we wait for 0.1s before checking the status again if wait_time < 10 and wait_time < wait: LOGGER.info( "Unexpected status {}. Waiting for 0.1s before rechecking status" .format(status)) time.sleep(0.1) continue else: raise exception_type( "Transaction submission failed with status '{}'" .format(status)) return response, signature if not exception_type: return response, signature else: if verbose: self._dbg_dump.dump_str("Transaction submission timeout") raise exception_type("Transaction submission timeout") response = self.send_request( "batches", batch_list.SerializeToString(), 'application/octet-stream', ) return response, signature def _create_batch_list(self, transactions): transaction_signatures = [t.header_signature for t in transactions] header = BatchHeader( signer_public_key=self.get_signer_public_key_as_hex(), transaction_ids=transaction_signatures).SerializeToString() signature = self._signer.sign(header) batch = Batch(header=header, transactions=transactions, header_signature=signature) return BatchList(batches=[batch]) def execute_json_transaction(self, json_input, address_family, wait, exception_type=None, verbose=False, timeout_exception_type=TimeoutError, transaction_output_list=None, transaction_input_list=None, transaction_dependency_list=None): json_dict = json.loads(json_input) verb = json_dict['verb'] if not verb: if not exception_type: return False raise exception_type("no 'verb' in the json input") if address_family == self.get_enclave_registry_family_name(): txn = PdoContractEnclaveTransaction() txn.verb = verb txn.verifying_key = json_dict.get("verifying_key") if verb == 'register': details = PdoContractEnclaveRegister() proof_data = json_dict.get("proof_data") if proof_data is None or isinstance(proof_data, str): json_format.Parse(json_input, details, ignore_unknown_fields=True) else: if not exception_type: return False raise exception_type("missing or invalid 'proof_data'") elif verb == 'update': details = PdoContractEnclaveUpdate() json_format.Parse(json_input, details, ignore_unknown_fields=True) elif verb == 'delete': details = None else: if not exception_type: return False raise exception_type( "unknown verb in the json input '{}'".format(verb)) if details: txn.transaction_details = txn.transaction_details = details.SerializeToString( ) if verbose: self._dbg_dump.dump_contract_enclave_transaction(txn) self._dbg_dump.dump_enclave_transaction_protobuf_message_to_json( txn) elif address_family == self.get_contract_registry_family_name(): txn = PdoContractTransaction() txn.verb = verb if 'contract_id' in json_dict: txn.contract_id = json_dict.get("contract_id") if verb == 'register': details = PdoContractRegister() elif verb == 'add-enclaves': details = PdoContractAddEnclaves() elif verb == 'remove-enclaves': details = PdoContractRemoveEnclaves() elif verb == 'delete': details = None else: if not exception_type: return False raise exception_type( "unknown verb in the json input '{}'".format(verb)) if details: json_format.Parse(json_input, details, ignore_unknown_fields=True) txn.transaction_details = details.SerializeToString() if verbose: self._dbg_dump.dump_contract_transaction(txn) self._dbg_dump.dump_contract_transaction_protobuf_message_to_json( txn) elif address_family == self.get_ccl_family_name(): if verb not in ['initialize', 'update', 'terminate', 'delete']: if not exception_type: return False raise exception_type( "unknown verb in the json input '{}'".format(verb)) txn = CCL_TransactionPayload() json_format.Parse(json_input, txn, ignore_unknown_fields=True) if verbose: self._dbg_dump.dump_ccl_transaction(txn) self._dbg_dump.dump_ccl_transaction_protobuf_message_to_json( txn) else: if not exception_type: return False raise exception_type( "unknown 'af' (a.k.a. address family) in the json input '{}'". format(address_family)) result, signature = self.send_transaction( txn.SerializeToString(), address_family, wait=wait, exception_type=timeout_exception_type, transaction_output_list=transaction_output_list, transaction_input_list=transaction_input_list, transaction_dependency_list=transaction_dependency_list) if verbose: self._dbg_dump.dump_str("") self._dbg_dump.dump_str(result) self._dbg_dump.dump_str("") self._dbg_dump.dump_str( "Transaction signature: {}".format(signature)) return signature
class ContractEnclaveRegistryTransactionHandler(TransactionHandler): def __init__(self, debug_on, dbg_dump_to_logger=True): self.connect = PdoTpConnectHelper() self._debug_on = debug_on if dbg_dump_to_logger: self.dbg_dump = PdoDbgDump(LOGGER) else: self.dbg_dump = PdoDbgDump() LOGGER.debug("Enclave namespace prefix: %s", self.connect.get_enclave_prefix()) @property def family_name(self): family = self.connect.get_enclave_registry_family_name() LOGGER.debug("Enclave family name: %s", family) return family @property def family_versions(self): return ['1.0'] @property def namespaces(self): return self.connect.get_enclave_prefix() def _get_enclave_state(self, context, enclave_id): address = self.connect.get_enclave_address(enclave_id) return self.connect.get_state(context, address, PdoContractEnclaveInfo) def _delete_enclave_state(self, context, enclave_id): address = self.connect.get_enclave_address(enclave_id) return self.connect.delete_state(context, address) def _set_enclave_state(self, context, enclave_id, data): address = self.connect.get_enclave_address(enclave_id) return self.connect.set_state(context, address, data) def _verify_registration_info(self, payload, details, public_key_hash, context): # TODO: Allowing no proof data should be removed in the production version if not details.proof_data: LOGGER.debug("*** Enclave proof data is empty - simulation mode") if not self._debug_on: raise InvalidTransaction( 'Simulation mode is not allowed when the debug support is OFF') return # Try to get the report key from the configuration setting. # If it is not there, fail verification. try: report_public_key_pem = self.connect.get_report_public_key(context) except KeyError: raise \ ValueError( 'Failed to get report public key configuration setting {}'.format( self.connect.get_report_public_key_setting_name())) # Retrieve the valid enclave measurement values, converting the comma- # delimited list. If it is not there, fail verification. try: valid_measurements = self.connect.get_valid_measurements(context) except KeyError: raise \ ValueError( 'Failed to get enclave measurements setting {}'.format( self.connect.get_valid_measurements_setting_name())) # Retrieve the valid enclave basename value. If it is not there, # fail verification. try: valid_basenames = self.connect.get_valid_basenames(context) except KeyError: raise \ ValueError( 'Failed to get enclave basenames setting {}'.format( self.connect.get_valid_basenames_setting_name())) verify_enclave_registration_info(self.connect, payload, details, public_key_hash, context, report_public_key_pem, valid_measurements, valid_basenames) # def check_address(context, address, key, register_new): def check_address(self, context, key, register_new): try: state = self._get_enclave_state(context, key) if register_new: if state.verifying_key: raise InvalidTransaction( 'Contract enclave already exist with signing_key {}'\ .format(key)) else: return state else: if not state.verifying_key: raise InvalidTransaction( 'Enclave does not exist: {0}'.format(key)) else: return state except InternalError: if not register_new: raise InvalidTransaction( 'Enclave does not exist: {0}'.format(key)) else: return PdoContractEnclaveInfo() def apply(self, transaction, context): txn_header = transaction.header txn_signer_public_key = txn_header.signer_public_key payload = PdoContractEnclaveTransaction() payload.ParseFromString(transaction.payload) self.dbg_dump.dump_contract_enclave_transaction(payload) info = self.check_address(context, payload.verifying_key, payload.verb == 'register') if payload.verb == 'register': public_key_hash = hashlib.sha256(txn_signer_public_key.encode()).hexdigest() details = PdoContractEnclaveRegister() details.ParseFromString(payload.transaction_details) try: self._verify_registration_info(payload, details, public_key_hash, context) except ValueError as error: raise InvalidTransaction\ ('Invalid Signup Info: {}'.format(error)) info.verifying_key = payload.verifying_key info.encryption_key = details.encryption_key info.last_registration_block_context = \ details.registration_block_context info.owner_id = txn_signer_public_key info.registration_transaction_id = transaction.signature self.dbg_dump.dump_contract_enclave_state(info, "Setting new PdoContractEnclaveInfo") self._set_enclave_state(context, payload.verifying_key, info.SerializeToString()) elif payload.verb == 'delete' or payload.verb == 'update': self.dbg_dump.dump_contract_enclave_state(info, "Received PdoContractEnclaveInfo") if payload.verb == 'delete': if not self._debug_on: raise InvalidTransaction('Delete is not allowed, debug support is OFF') LOGGER.info("Deleting PdoContractEnclaveInfo %s", payload.verifying_key) self._delete_enclave_state(context, payload.verifying_key) else: # Check the contract enclave owner matches transaction signer. if info.owner_id != txn_signer_public_key: raise InvalidTransaction( 'Owner signature mismatch signer {}, owner {}' .format(info.verifying_key, txn_signer_public_key)) details = PdoContractEnclaveUpdate() details.ParseFromString(payload.transaction_details) info.last_registration_block_context = \ details.registration_block_context self.dbg_dump.dump_contract_enclave_state(info, "Updating existing PdoContractEnclaveInfo") self._set_enclave_state(context, payload.verifying_key, info.SerializeToString()) else: raise InvalidTransaction('Invalid transaction action {}' .format(payload.verb))