Esempio n. 1
0
    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)
Esempio n. 2
0
    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
Esempio n. 3
0
    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
Esempio n. 4
0
    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')
Esempio n. 6
0
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"]}'
    )