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 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 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 )
async def put_service( request: Request, service_id: int, certchain: CertChainRequestModel, auth: ServiceRequestAuthFast = Depends(ServiceRequestAuthFast), db_session=Depends(asyncdb_session)): ''' Registers a known service with its IP address and its data cert ''' _LOGGER.debug(f'PUT Service API called from {request.client.host}') await auth.authenticate() network: Network = config.server.network dnsdb: DnsDb = config.server.network.dnsdb if service_id != auth.service_id: raise ValueError(f'Service ID {service_id} of PUT call does not match ' f'service ID {auth.service_id} in client cert') if service_id not in network.services: # So this worker process does not know about the service. Let's # see if a CSR for the service secret has previously been signed # and the resulting cert saved if not Service.is_registered(service_id): raise ValueError(f'Registration for unknown service: {service_id}') service = Service(network, service_id=service_id) service.registration_status = RegistrationStatus.CsrSigned network.services[service_id] = service else: service = network.services.get(service_id) if service is None: raise ValueError(f'Unkown service id: {service_id}') if not service.data_secret: service.data_secret = ServiceDataSecret(service.name, service_id, network) service.data_secret.from_string(certchain.certchain) await service.data_secret.save(overwrite=True) _LOGGER.debug( f'Updating registration for service id {service_id} with remote' f'address {auth.remote_addr}') await dnsdb.create_update(None, IdType.SERVICE, auth.remote_addr, db_session, service_id=service_id) return {'ipv4_address': auth.remote_addr}
async def setup(self, local_service_contract: str = None): if self.service_id not in self.network.services: # Here we read the service contract as currently published # by the service, which may differ from the one we have # previously accepted if local_service_contract: if not config.test_case: raise ValueError( 'Sideloading service contract only supported for ' 'test cases' ) filepath = local_service_contract verify_signatures = False else: filepath = self.paths.service_file(self.service_id) verify_signatures = True try: self.service = await Service.get_service( self.network, filepath=filepath, verify_signatures=verify_signatures ) if not local_service_contract: await self.service.verify_schema_signatures() except FileNotFoundError: # if the service contract is not yet available for # this membership then it should be downloaded at # a later point _LOGGER.info( f'Service contract file {filepath} does not exist' ) self.service = Service( self.network, service_id=self.service_id, ) await self.service.download_data_secret( save=True, failhard=False ) self.network.services[self.service_id] = self.service # This is the schema a.k.a data contract that we have previously # accepted, which may differ from the latest schema version offered # by the service try: self.schema: Schema = await self.load_schema() except FileNotFoundError: # We do not have the schema file for a service that the pod did # not join yet pass self.service = self.network.services[self.service_id] # We need the service data secret to verify the signature of the # data contract we have previously accepted self.service_data_secret = ServiceDataSecret( None, self.service_id, self.network ) if await self.service_data_secret.cert_file_exists(): await self.service_data_secret.load(with_private_key=False) elif not local_service_contract: await self.service.download_data_secret(save=True) await self.service_data_secret.load(with_private_key=False) else: _LOGGER.debug( 'Not loading service data secret as we are sideloading the ' 'service contract' )
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 patch_service( request: Request, schema: SchemaModel, service_id: int, auth: ServiceRequestAuthFast = Depends(ServiceRequestAuthFast)): ''' Submit a new (revision of a) service schema, aka data contract for signing with the Network Data secret This API is called by services ''' _LOGGER.debug(f'PATCH Service API called from {request.client.host}') await auth.authenticate() # Authorize the request if service_id != auth.service_id: raise HTTPException( 401, 'Service ID query parameter does not match the client cert') if service_id != schema.service_id: raise HTTPException( 403, f'Service_ID query parameter {service_id} does not match ' f'the ServiceID parameter in the schema {schema.service_id}') # End of authorization network: Network = config.server.network # TODO: create a whole bunch of schema validation tests # including one to just deserialize and reserialize and # verify the signatures again status = ReviewStatusType.ACCEPTED errors = [] if not schema.signatures: status = ReviewStatusType.REJECTED errors.append('Missing service signature') else: if (not schema.signatures['service'] or not schema.signatures['service'].get('signature')): status = ReviewStatusType.REJECTED errors.append('Missing service signature') if schema.signatures.get('network'): status = ReviewStatusType.REJECTED errors.append('network signature already present') service_id = schema.service_id if service_id != auth.service_id: status = ReviewStatusType.REJECTED errors.append( f'Service ID {service_id} in schema does not match service ' f'id {auth.service_id} in client cert') else: if service_id not in network.services: # So this worker process does not know about the service. Let's # see if a CSR for the service secret has previously been signed # and the resulting cert saved if not Service.is_registered(service_id): service = None else: service = Service(network, service_id=service_id) service.registration_status = RegistrationStatus.CsrSigned # Add service to in-memory cache network.services[service_id] = service else: service = network.services.get(service_id) if not service: status = ReviewStatusType.REJECTED errors.append(f'Unregistered service ID {service_id}') else: if service.schema and schema.version <= service.schema.version: status = ReviewStatusType.REJECTED errors.append( f'Schema version {schema.version} is less than current ' f'schema version ') else: service_contract = Schema(schema.as_dict()) try: if not service.data_secret: service.data_secret = ServiceDataSecret( None, service_id, network) await service.data_secret.load(with_private_key=False) service_contract.verify_signature(service.data_secret, SignatureType.SERVICE) service.schema = service_contract service.schema.create_signature(network.data_secret, SignatureType.NETWORK) storage_driver = network.paths.storage_driver filepath = network.paths.get(Paths.SERVICE_FILE) await service_contract.save(filepath, storage_driver) except ValueError: status = ReviewStatusType.REJECTED errors.append('Service signature of schema is invalid') return { 'status': status, 'errors': errors, 'timestamp': datetime.now(timezone.utc).isoformat(), }