async def load_secrets(self, with_private_key: bool = True, password: str = None, service_ca_password=None) -> None: ''' Loads all the secrets of a service :param with_private_key: Load the private keys for all secrets except the Service CA key :param password: password to use for private keys of all secrets except the Service CA :param service_ca_password: optional password to use for private key of the Service CA. If not specified, only the cert of the Service CA will be loaded. ''' if not self.service_ca: self.service_ca = ServiceCaSecret( self.name, self.service_id, self.network ) if service_ca_password: await self.service_ca.load( with_private_key=True, password=service_ca_password ) else: await self.service_ca.load(with_private_key=False) if not self.apps_ca: self.apps_ca = AppsCaSecret( self.name, self.service_id, self.network ) await self.apps_ca.load( with_private_key=with_private_key, password=password ) if not self.members_ca: self.members_ca = MembersCaSecret( None, self.service_id, self.network ) await self.members_ca.load( with_private_key=with_private_key, password=password ) if not self.tls_secret: self.tls_secret = ServiceSecret( self.name, self.service_id, self.network ) await self.tls_secret.load( with_private_key=with_private_key, password=password ) if not self.data_secret: await self.load_data_secret(with_private_key, password=password) # We use the service secret as client TLS cert for outbound # requests. We only do this if we read the private key # for the TLS/service secret if with_private_key: filepath = self.tls_secret.save_tmp_private_key() config.requests.cert = (self.tls_secret.cert_file, filepath)
async def get_registration_status(self) -> RegistrationStatus: ''' Checks what the registration status if of a service in the Service server or the Directory server. ''' server = config.server if not self.schema: if await self.schema_file_exists(): if not self.data_secret or not self.data_secret.cert(): await self.load_data_secret( with_private_key=False, password=None ) await self.load_schema(self.paths.get(Paths.SERVICE_FILE)) if self.schema and self.schema.signatures.get('network'): return RegistrationStatus.SchemaSigned if server.server_type == ServerType.DIRECTORY: try: await self.network.dnsdb.lookup( None, IdType.SERVICE, DnsRecordType.A, None, service_id=self.service_id, ) return RegistrationStatus.Registered except KeyError: _LOGGER.debug(f'DB lookup of service {self.service_id} failed') else: fqdn = ServiceSecret.create_commonname( self.service_id, self.network.name ) try: socket.gethostbyname(fqdn) return RegistrationStatus.Registered except socket.gaierror: _LOGGER.debug(f'DNS lookup of {fqdn} failed') if not self.service_ca: self.service_ca = ServiceCaSecret(None, self.service_id, server.network) if await self.service_ca.cert_file_exists(): if await self.service_ca.private_key_file_exists(): # We must be running on a ServiceServer await self.service_ca.load( with_private_key=True, password=self.private_key_password ) else: await self.service_ca.load(with_private_key=False) return RegistrationStatus.CsrSigned else: if self.service_ca.cert: return RegistrationStatus.CsrSigned return RegistrationStatus.Unknown
def check_service_cert(self, network: Network) -> None: ''' Checks if the MTLS client certificate was signed the cert chain for members of the service :param network: the network that we are in :param service_id: the service_id parsed from the incoming request, if applicable :raises: HTTPException ''' if not self.client_cn or not self.issuing_ca_cn: raise HTTPException(status_code=401, detail='Missing MTLS client cert') # Check that the client common name is well-formed and # extract the service_id entity_id = ServiceSecret.parse_commonname(self.client_cn, network) try: # Service secret gets signed by Service CA service_ca_secret = ServiceCaSecret(None, entity_id.service_id, network=network) entity_id = service_ca_secret.review_commonname(self.client_cn) self.service_id = entity_id.service_id # Service CA secret gets signed by Network Services CA networkservices_ca_secret = NetworkServicesCaSecret(network.paths) networkservices_ca_secret.review_commonname(self.issuing_ca_cn) except ValueError as exc: raise HTTPException( status_code=403, detail=( f'Incorrect c_cn {self.client_cn} issued by ' f'{self.issuing_ca_cn} for service {self.service_id} on ' f'network {network.name}')) from exc
def compose_fqdn(self, uuid: UUID, id_type: IdType, service_id: Optional[int] = None) -> str: ''' Generate the FQDN for an id of the specified type :param uuid: identifier for the account or member. Must be None for IdType.SERVICE :param id_type: type of service :param service_id: identifier for the service, required for IdType.MEMBER and IdType.ACCOUNT :returns: FQDN :raises: (none) ''' self._validate_parameters(uuid, id_type, service_id=service_id) if id_type == IdType.MEMBER: return MemberSecret.create_commonname( uuid, service_id, self.domain ) elif id_type == IdType.ACCOUNT: return AccountSecret.create_commonname(uuid, self.domain) elif id_type == IdType.SERVICE: return ServiceSecret.create_commonname(service_id, self.domain)
async def test_network_service_creation(self): API = BASE_URL + '/v1/network/service' # We can not use deepcopy here so do two copies network = copy(config.server.network) network.paths = copy(config.server.network.paths) network.paths._root_directory = SERVICE_DIR if not await network.paths.secrets_directory_exists(): await network.paths.create_secrets_directory() service_id = SERVICE_ID serviceca_secret = ServiceCaSecret(service='dir_api_test', service_id=service_id, network=network) csr = serviceca_secret.create_csr() csr = csr.public_bytes(serialization.Encoding.PEM) response = requests.post(API, json={'csr': str(csr, 'utf-8')}) self.assertEqual(response.status_code, 201) data = response.json() issuing_ca_cert = x509.load_pem_x509_certificate( # noqa:F841 data['cert_chain'].encode()) serviceca_cert = x509.load_pem_x509_certificate( # noqa:F841 data['signed_cert'].encode()) # TODO: populate a secret from a CertChain serviceca_secret.cert = serviceca_cert serviceca_secret.cert_chain = [issuing_ca_cert] network_data_cert = x509.load_pem_x509_certificate( # noqa:F841 data['network_data_cert_chain'].encode()) # Check that the service CA public cert was written to the network # directory of the dirserver testsecret = ServiceCaSecret(service='dir_api_test', service_id=service_id, network=config.server.network) await testsecret.load(with_private_key=False) service_secret = ServiceSecret('dir_api_test', service_id, network) service_csr = service_secret.create_csr() certchain = serviceca_secret.sign_csr(service_csr) service_secret.from_signed_cert(certchain) await service_secret.save() service_cn = Secret.extract_commonname(certchain.signed_cert) serviceca_cn = Secret.extract_commonname(serviceca_cert) # Create and register the the public cert of the data secret, # which the directory server needs to validate the service signature # of the schema for the service service_data_secret = ServiceDataSecret('dir_api_test', service_id, network) service_data_csr = service_data_secret.create_csr() data_certchain = serviceca_secret.sign_csr(service_data_csr) service_data_secret.from_signed_cert(data_certchain) await service_data_secret.save() headers = { 'X-Client-SSL-Verify': 'SUCCESS', 'X-Client-SSL-Subject': f'CN={service_cn}', 'X-Client-SSL-Issuing-CA': f'CN={serviceca_cn}' } data_certchain = service_data_secret.certchain_as_pem() response = requests.put(API + '/service_id/' + str(service_id), headers=headers, json={'certchain': data_certchain}) self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(data['ipv4_address'], '127.0.0.1') # Send the service schema with open(DEFAULT_SCHEMA) as file_desc: data = file_desc.read() schema_data = orjson.loads(data) schema_data['service_id'] = service_id schema_data['version'] = 1 schema = Schema(schema_data) schema.create_signature(service_data_secret, SignatureType.SERVICE) headers = { 'X-Client-SSL-Verify': 'SUCCESS', 'X-Client-SSL-Subject': f'CN={service_cn}', 'X-Client-SSL-Issuing-CA': f'CN={serviceca_cn}' } response = requests.patch(API + f'/service_id/{service_id}', headers=headers, json=schema.json_schema) self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(data['status'], 'ACCEPTED') self.assertEqual(len(data['errors']), 0) # Get the fully-signed data contract for the service API = BASE_URL + '/v1/network/service' response = requests.get(API + f'/service_id/{service_id}') self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(len(data), 10) self.assertEqual(data['service_id'], SERVICE_ID) self.assertEqual(data['version'], 1) self.assertEqual(data['name'], 'dummyservice') self.assertEqual(len(data['signatures']), 2) schema = Schema(data) # Get the list of service summaries API = BASE_URL + '/v1/network/services' response = requests.get(API) self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(len(data), 1) service_summary = data['service_summaries'][0] self.assertEqual(service_summary['service_id'], SERVICE_ID) self.assertEqual(service_summary['version'], 1) self.assertEqual(service_summary['name'], 'dummyservice') # Now test membership registration against the directory server API = BASE_URL + '/v1/network/member' headers = { 'X-Client-SSL-Verify': 'SUCCESS', 'X-Client-SSL-Subject': f'CN={uuid4()}.members-{service_id}.{network.name}', 'X-Client-SSL-Issuing-CA': f'CN=members-ca.members-ca-{service_id}.{network.name}' } response = requests.put(API, headers=headers) self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(data['ipv4_address'], '127.0.0.1')
class Service: ''' Models a service on a BYODA network. This class is used both by the SDK for hosting a service and by pods ''' def __init__(self, network: Network = None, service_id: int = None, storage_driver: FileStorage = None): ''' Constructor, can be used by the service but also by the network, an app or an account or member to model the service. Because of this, only minimal initiation of the instance is done and depending on the user case, additional methods must be called to load all the needed info for the service. :param network: Network the service is part of :param filepath: the file with the service schema/contract. If this optional parameter is specified, the signatures of the schema/contract will not be verified. :param service_id: the service_id for the service ''' self.name: str = None self.service_id: int = service_id self.registration_status: RegistrationStatus = \ RegistrationStatus.Unknown # The data contract for the service. TODO: versioned schemas self.schema: Schema = None # Was the schema for the service signed self.signed: bool = None self.private_key_password: str = network.private_key_password # The CA signed by the Services CA of the network self.service_ca: ServiceCaSecret = None # CA signs secrets of new members of the service self.members_ca: MembersCaSecret = None # CA signs secrets of apps that run with a delegation of # the data contract of the service self.apps_ca: AppsCaSecret = None # The secret used as server cert for incoming TLS connections # and as client cert in outbound TLS connections self.tls_secret: ServiceSecret = None # The secret used to sign documents, ie. the data contract for # the service self.data_secret: ServiceDataSecret = None # The network that the service is a part of. As storage is already # set up for the Network object, we can copy it here for the Service self.network: Network = network self.paths: Paths = copy(network.paths) self.paths.service_id = self.service_id if storage_driver: self.storage_driver = storage_driver else: self.storage_driver = self.paths.storage_driver async def examine_servicecontract(self, filepath: str) -> None: ''' Extracts the name and the service ID from the service contract ''' raw_data = await self.storage_driver.read(filepath) data = orjson.loads(raw_data) self.service_id = int(data['service_id']) self.name = data['name'] @classmethod async def get_service(cls, network: Network, filepath: str = None, verify_signatures: bool = True, with_private_key: bool = False, password: str = None): ''' Factory for Service class, loads the service metadata from a local file and verifies its signatures :param network: the network to which service belongs :param filepath: path to the file containing the data contract :param verify_signatures: should the data secret be loaded so it can be used to validate the service signature of the service contract? This parameter must only be set to False by test cases ''' if not verify_signatures and not config.test_case: raise ValueError( 'verify_signatures should only be False for test cases' ) service = Service(network=network) if filepath: await service.examine_servicecontract(filepath) if verify_signatures: await service.load_data_secret(with_private_key, password) await service.load_schema( filepath=filepath, verify_contract_signatures=verify_signatures ) service.schema.generate_graphql_schema(verify_schema_signatures=verify_signatures) _LOGGER.debug(f'Read service from {filepath}') return service @property def fqdn(self): return self.tls_secret.common_name async def load_schema(self, filepath: str = None, verify_contract_signatures: bool = True) -> bool: ''' Loads the schema for a service :returns: whether schema was signed ''' # TODO: implement validation of the service definition using # JSON-Schema meta schema if filepath is None: if not verify_contract_signatures: raise ValueError( 'The signatures for Schemas downloaded from the network ' 'must always be validated' ) raise NotImplementedError( 'Downloading service definitions from the directory server ' 'of a network is not yet implemented' ) self.schema = await Schema.get_schema( filepath, self.storage_driver, service_data_secret=self.data_secret, network_data_secret=self.network.data_secret, verify_contract_signatures=verify_contract_signatures ) self.name = self.schema.name self.service_id = int(self.schema.service_id) self.paths.service_id = self.service_id _LOGGER.debug( f'Read service {self.name} wih service_id {self.service_id}' ) if verify_contract_signatures: await self.verify_schema_signatures() self.registration_status = RegistrationStatus.SchemaSigned async def save_schema(self, data: str, filepath: str = None): ''' Saves the raw data of the service contract to the Service directory ''' if not filepath: filepath = self.paths.get(Paths.SERVICE_FILE, service_id=self.service_id) await self.storage_driver.write(filepath, data) async def verify_schema_signatures(self): ''' Verify the signatures for the schema, a.k.a. data contract :raises: ValueError ''' if not self.schema.signatures[SignatureType.SERVICE.value]: raise ValueError('Schema does not contain a service signature') if not self.schema.signatures[SignatureType.NETWORK.value]: raise ValueError('Schema does not contain a network signature') if not self.data_secret or not self.data_secret.cert: # Let's see if we can read the data secret ourselves self.data_secret = ServiceDataSecret(None, self.service_id, self.network) await self.data_secret.load(with_private_key=False) if not self.network.data_secret or not self.network.data_secret.cert: self.network.data_secret = NetworkDataSecret(self.network.paths) await self.network.data_secret.load(with_private_key=False) self.schema.verify_signature(self.data_secret, SignatureType.SERVICE) _LOGGER.debug( 'Verified service signature for service %s', self.service_id ) self.schema.verify_signature( self.network.data_secret, SignatureType.NETWORK ) _LOGGER.debug( 'Verified network signature for service %s', self.service_id ) def validate(self, data: Dict): ''' Validates the data against the json schema for the service ''' self.schema.validate(data) async def schema_file_exists(self) -> bool: ''' Check if the file with the schema exists on the local file system ''' server = config.server if server.server_type not in (ServerType.SERVICE, ServerType.DIRECTORY): raise ValueError( 'This function should only be called from Directory- and ' f'Service-servers, not from a {type(server)}' ) filepath = self.paths.get(Paths.SERVICE_FILE, service_id=self.service_id) return os.path.exists(filepath) async def create_secrets(self, network_services_ca: NetworkServicesCaSecret, local: bool = False, password: str = None) -> None: ''' Creates all the secrets of a service :raises RuntimeError, PermissionError ''' if (self.service_ca or self.members_ca or self.apps_ca or self.tls_secret or self.data_secret): raise RuntimeError('One or more service secrets already exist') if password: self.private_key_password = password if not await self.paths.service_directory_exists(self.service_id): await self.paths.create_service_directory(self.service_id) if not await self.paths.secrets_directory_exists(): await self.paths.create_secrets_directory() await self.create_service_ca(network_services_ca, local=True) await self.create_apps_ca() await self.create_members_ca() await self.create_tls_secret() await self.create_data_secret() async def create_service_ca(self, network_services_ca: NetworkServicesCaSecret = None, local: bool = False) -> None: ''' Create the service CA using a generated password for the private key. This password is different then the passwords for the other secrets as the Service CA should have additional security implemented and should be stored off-line :param local: should the CSR be signed by a local key or using a request to the directory server of the network :raises: ValueError if the service ca already exists ''' private_key_password = passgen.passgen(length=48) if local: self.service_ca = await self._create_secret( ServiceCaSecret, network_services_ca, private_key_password=private_key_password ) else: self.service_ca = await self._create_secret( ServiceCaSecret, None, private_key_password=private_key_password ) _LOGGER.info( '!!! Private key password for the off-line Service CA: ' f'{private_key_password}' ) async def create_members_ca(self) -> None: ''' Creates the member CA, signed by the Service CA :raises: ValueError if no Service CA is available to sign the CSR of the member CA ''' self.members_ca = await self._create_secret( MembersCaSecret, self.service_ca, private_key_password=self.private_key_password ) async def create_apps_ca(self) -> None: ''' Create the CA that signs application secrets ''' self.apps_ca = await self._create_secret( AppsCaSecret, self.service_ca, private_key_password=self.private_key_password ) async def create_tls_secret(self) -> None: ''' Creates the service TLS secret, signed by the Service CA :raises: ValueError if no Service CA is available to sign the CSR of the service secret ''' self.tls_secret = await self._create_secret( ServiceSecret, self.service_ca, private_key_password=self.private_key_password ) async def create_data_secret(self) -> None: ''' Creates the service data secret, signed by the Service CA :raises: ValueError if no Service CA is available to sign the CSR of the service secret ''' self.data_secret = await self._create_secret( ServiceDataSecret, self.service_ca, private_key_password=self.private_key_password ) async def _create_secret(self, secret_cls: Callable, issuing_ca: Secret, private_key_password: str = None) -> Secret: ''' Abstraction for creating secrets for the Service class to avoid repetition of code for creating the various member secrets of the Service class :param secret_cls: callable for one of the classes derived from byoda.util.secrets.Secret :raises: ValueError, NotImplementedError ''' if not self.name or self.service_id is None: raise ValueError( 'Name and service_id of the service have not been defined' ) secret = secret_cls( self.name, self.service_id, network=self.network ) if await secret.cert_file_exists(): raise ValueError( f'{type(secret)} cert for {self.name} ({self.service_id}) ' 'already exists' ) if await secret.private_key_file_exists(): raise ValueError( f'{type(secret)} key for {self.name} ({self.service_id}) ' 'already exists' ) # TODO: SECURITY: add constraints csr = secret.create_csr() await self.get_csr_signature( secret, csr, issuing_ca, private_key_password=private_key_password ) return secret async def get_csr_signature(self, secret: Secret, csr: CSR, issuing_ca: CaSecret, private_key_password: str = None) -> None: ''' Gets the signed cert(chain) for the CSR and saves returned cert and the existing private key. If the issuing_ca parameter is specified then the CSR will be signed directly by the private key of the issuing_ca, otherwise the CSR will be send in a POST /api/v1/network/service API call to the directory server of the network ''' if (isinstance(secret, ServiceCaSecret) and ( self.registration_status != RegistrationStatus.Unknown or await secret.cert_file_exists())): # TODO: support renewal of ServiceCA cert raise ValueError('ServiceCA cert has already been signed') if issuing_ca: # We have the private key of the issuing CA so can # sign ourselves and be done with it issuing_ca.review_csr(csr, source=CsrSource.LOCAL) certchain = issuing_ca.sign_csr(csr) secret.from_signed_cert(certchain) await secret.save(password=private_key_password, overwrite=False) # We do not set self.registration_status as locally signing # does not provide information about the status of service in # the network return if not isinstance(secret, ServiceCaSecret): raise ValueError( f'No issuing_ca was provided for creating a ' f'{type(secret)}' ) # We have to get our signature from the directory server data = { 'csr': str( csr.public_bytes(serialization.Encoding.PEM), 'utf-8' ) } url = self.paths.get(Paths.NETWORKSERVICE_POST_API) response = await RestApiClient.call( url, HttpMethod.POST, data=data ) if response.status != 201: raise ValueError( f'Failed to POST to API {Paths.NETWORKSERVICE_API}: ' f'{response.status}' ) data = await response.json() secret.from_string(data['signed_cert'] + data['cert_chain']) self.registration_status = RegistrationStatus.CsrSigned await secret.save(password=private_key_password, overwrite=False) # Every time we receive the network data cert, we # save it as it could have changed since the last time we # got it network = config.server.network if not network.data_secret: network.data_secret = NetworkDataSecret(network.paths) network.data_secret.from_string(data['network_data_cert_chain']) await network.data_secret.save(overwrite=True) @staticmethod async def is_registered(service_id: int, registration_status: RegistrationStatus = None) -> bool: ''' Checks is the service is registered in the network. When running on the directory server, this can check whether a CSR was signed, whether an IP address for the service registered and where the serice schema/data contract was signed by the network. :param registration_status: if not defined, any status except for RegistrationStatus.Unknown will result in True being returned. It is not allowed to specify RegistrationStatus.Unknown. For other values, the value is compared to the registration status of the service ''' server = config.server network = config.server.network if registration_status == RegistrationStatus.Unknown: raise ValueError('Can not check on unknown registration status') if server.server_type not in (ServerType.DIRECTORY, ServerType.SERVICE): if registration_status != RegistrationStatus.SchemaSigned: raise ValueError( f'Can not check registration status {registration_status.value} ' f'on servers of type {type(server)}' ) service = Service(network, service_id=service_id) status = await service.get_registration_status() if status == RegistrationStatus.Unknown: return False if not registration_status: return True return status == registration_status.value async def get_registration_status(self) -> RegistrationStatus: ''' Checks what the registration status if of a service in the Service server or the Directory server. ''' server = config.server if not self.schema: if await self.schema_file_exists(): if not self.data_secret or not self.data_secret.cert(): await self.load_data_secret( with_private_key=False, password=None ) await self.load_schema(self.paths.get(Paths.SERVICE_FILE)) if self.schema and self.schema.signatures.get('network'): return RegistrationStatus.SchemaSigned if server.server_type == ServerType.DIRECTORY: try: await self.network.dnsdb.lookup( None, IdType.SERVICE, DnsRecordType.A, None, service_id=self.service_id, ) return RegistrationStatus.Registered except KeyError: _LOGGER.debug(f'DB lookup of service {self.service_id} failed') else: fqdn = ServiceSecret.create_commonname( self.service_id, self.network.name ) try: socket.gethostbyname(fqdn) return RegistrationStatus.Registered except socket.gaierror: _LOGGER.debug(f'DNS lookup of {fqdn} failed') if not self.service_ca: self.service_ca = ServiceCaSecret(None, self.service_id, server.network) if await self.service_ca.cert_file_exists(): if await self.service_ca.private_key_file_exists(): # We must be running on a ServiceServer await self.service_ca.load( with_private_key=True, password=self.private_key_password ) else: await self.service_ca.load(with_private_key=False) return RegistrationStatus.CsrSigned else: if self.service_ca.cert: return RegistrationStatus.CsrSigned return RegistrationStatus.Unknown async def register_service(self): ''' Registers the service with the network using the Service TLS secret :raises: ValueError if the function is not called by a ServerType.SERVICE ''' server = config.server if server and not server.server_type == ServerType.SERVICE: raise ValueError('Only Service servers can register a service') if self.registration_status == RegistrationStatus.Unknown: raise ValueError( 'Can not register a service before its CSR has been signed ' 'by the network' ) key_path = self.tls_secret.save_tmp_private_key() data_certchain = {'certchain': self.data_secret.certchain_as_pem()} url = self.paths.get(Paths.NETWORKSERVICE_API) response = await RestApiClient.call( url, HttpMethod.PUT, secret=self.tls_secret, data=data_certchain, service_id=self.service_id ) return response async def download_schema(self, save: bool = True, filepath: str = None) -> str: ''' Downloads the latest schema from the webserver of the service :param filepath: location where to store the schema. If not specified, the default location will be used :returns: the schema as string ''' if save: # Resolve any variables in the value for the filepath variable if not filepath: filepath = self.paths.get(Paths.SERVICE_FILE, self.service_id) else: filepath = self.paths.get(filepath, service_id=self.service_id) resp = await ApiClient.call( Paths.SERVICE_CONTRACT_DOWNLOAD, service_id=self.service_id ) if resp.status == 200: if save: await self.save_schema(await resp.text(), filepath=filepath) return await resp.text() raise FileNotFoundError( f'Download of service schema failed: {resp.status}' ) async def load_secrets(self, with_private_key: bool = True, password: str = None, service_ca_password=None) -> None: ''' Loads all the secrets of a service :param with_private_key: Load the private keys for all secrets except the Service CA key :param password: password to use for private keys of all secrets except the Service CA :param service_ca_password: optional password to use for private key of the Service CA. If not specified, only the cert of the Service CA will be loaded. ''' if not self.service_ca: self.service_ca = ServiceCaSecret( self.name, self.service_id, self.network ) if service_ca_password: await self.service_ca.load( with_private_key=True, password=service_ca_password ) else: await self.service_ca.load(with_private_key=False) if not self.apps_ca: self.apps_ca = AppsCaSecret( self.name, self.service_id, self.network ) await self.apps_ca.load( with_private_key=with_private_key, password=password ) if not self.members_ca: self.members_ca = MembersCaSecret( None, self.service_id, self.network ) await self.members_ca.load( with_private_key=with_private_key, password=password ) if not self.tls_secret: self.tls_secret = ServiceSecret( self.name, self.service_id, self.network ) await self.tls_secret.load( with_private_key=with_private_key, password=password ) if not self.data_secret: await self.load_data_secret(with_private_key, password=password) # We use the service secret as client TLS cert for outbound # requests. We only do this if we read the private key # for the TLS/service secret if with_private_key: filepath = self.tls_secret.save_tmp_private_key() config.requests.cert = (self.tls_secret.cert_file, filepath) async def load_data_secret(self, with_private_key: bool, password: str = None, download: bool = False) -> None: ''' Loads the certificate of the data secret of the service ''' if with_private_key and not password: raise ValueError('Can not read data secret private key without password') if not self.data_secret: self.data_secret = ServiceDataSecret( self.name, self.service_id, self.network ) if not await self.data_secret.cert_file_exists(): if download: if with_private_key: raise ValueError( 'Can not download private key of the secret from ' 'the network' ) await self.download_data_secret() else: _LOGGER.exception( 'Could not read service data secret for service: ' f'{self.service_id}: {self.data_secret.cert_file}' ) raise FileNotFoundError(self.data_secret.cert_file) else: await self.data_secret.load( with_private_key=with_private_key, password=password ) async def download_data_secret(self, save: bool = True, failhard: bool = False) -> str: ''' Downloads the data secret from the web service for the service :returns: the cert in PEM format ''' try: resp = await ApiClient.call( Paths.SERVICE_DATACERT_DOWNLOAD, service_id=self.service_id ) except RuntimeError: if failhard: raise else: return None if resp.status == 200: if save: self.data_secret = ServiceDataSecret(None, self.service_id, self.network) self.data_secret.from_string(await resp.text()) await self.data_secret.save(overwrite=(not failhard)) return await resp.text() raise FileNotFoundError( f'Could not download data cert for service {self.service_id}: ' f'{resp.status}' )
async def create_network_signature(service, args, password) -> bool: ''' Add network signature to the service schema/data contract, either locally or by a directory server over the network :returns: was signing the schema/contract successful? ''' network = service.network if (SignatureType.SERVICE.value not in service.schema.json_schema['signatures']): raise ValueError('Schema has not been signed by the service') if (service.schema.json_schema['signatures'].get( SignatureType.NETWORK.value)): raise ValueError('Schema has already been signed by the network') # We first verify the service signature before we add the network # signature _LOGGER.debug('Verifying service signature') service.schema.verify_signature( service.data_secret, SignatureType.SERVICE ) _LOGGER.debug('Service signature has been verified') if args.local: # When signing locally, the service contract gets updated # with the network signature _LOGGER.debug('Locally creating network signature') network.data_secret = NetworkDataSecret(network.paths) await network.data_secret.load( with_private_key=True, password=password ) service.schema.create_signature( network.data_secret, SignatureType.NETWORK ) else: service_secret = ServiceSecret(None, service.service_id, network) await service_secret.load(with_private_key=True, password=password) _LOGGER.debug('Requesting network signature from the directory server') resp = await RestApiClient.call( service.paths.get(Paths.NETWORKSERVICE_API), HttpMethod.PATCH, secret=service_secret, data=service.schema.json_schema, ) if resp.status != 200: return False data = await resp.json() if data['errors']: _LOGGER.debug('Validation of service by the network failed') for error in data['errors']: _LOGGER.debug(f'Validation error: {error}') return False else: resp = await RestApiClient.call( service.paths.get(Paths.NETWORKSERVICE_API), HttpMethod.GET, secret=service_secret, service_id=service.service_id ) if resp.status == 200: service.schema.json_schema = await resp.json() service.registration_status = \ RegistrationStatus.SchemaSigned return True else: return False _LOGGER.debug( 'Added network signature ' f'{service.schema.json_schema["signatures"]["network"]}' )