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}
Exemple #5
0
    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(),
    }