예제 #1
0
    def sign(self, csr: str, id_type: IdType,
             remote_addr: IpAddress) -> CertChain:
        '''
        Evaluate a CSR and sign it

        :param csr: the Certificate Signing Request
        :param id_type: what entity is the CSR for, client, service or member
        :param remote_addr: the originating IP address for the CSR
        :returns: the signed certificate and its certchain
        :raises: KeyError if the Certificate Name is not acceptable,
                 ValueError if there is something else unacceptable in the CSR
        '''

        if type(csr) not in (str, bytes):
            raise ValueError('CSR must be a string or a byte array')

        cert_auth = self.ca_secret

        csr = Secret.csr_from_string(csr)

        extension = csr.extensions.get_extension_for_class(
            x509.BasicConstraints)
        if not cert_auth.signs_ca_certs and extension.value.ca:
            raise ValueError('Certificates with CA bits set are not permitted')

        entity_id = cert_auth.review_csr(csr)

        if entity_id.id_type == IdType.SERVICE:
            raise NotImplementedError(
                'Service certs are not supported for this API, '
                'only ServiceCA certs')

        # TODO: add check on whether the UUID is already in use
        certchain = cert_auth.sign_csr(csr, 365 * 3)

        id_type = entity_id.id_type.value.strip('-')
        _LOGGER.info(f'Signed the CSR for {entity_id.id} for IdType {id_type} '
                     f'received from IP {str(remote_addr)}')
        return certchain
예제 #2
0
async def post_service(request: Request,
                       csr: CertSigningRequestModel,
                       auth: ServiceRequestOptionalAuthFast = Depends(
                           ServiceRequestOptionalAuthFast),
                       db_session=Depends(asyncdb_session)):
    '''
    Submit a Certificate Signing Request for the ServiceCA certificate
    and get the cert signed by the network services CA
    This API is called by services
    This API does not require authentication, it needs to be rate
    limited by the reverse proxy
    '''

    _LOGGER.debug(f'POST Service API called from {request.client.host}')

    network = config.server.network
    dnsdb: DnsDb = config.server.network.dnsdb

    # Authorization
    csr_x509: x509 = Secret.csr_from_string(csr.csr)
    common_name = Secret.extract_commonname(csr_x509)

    try:
        entity_id = NetworkServicesCaSecret.review_commonname_by_parameters(
            common_name, network.name)
    except PermissionError:
        raise HTTPException(status_code=401,
                            detail=f'Invalid common name {common_name} in CSR')
    except (ValueError, KeyError):
        raise HTTPException(
            status_code=400,
            detail=(
                f'error when reviewing the common name {common_name} in your '
                'CSR'))

    try:
        await dnsdb.lookup_fqdn(common_name, DnsRecordType.A, db_session)
        dns_exists = True
    except KeyError:
        dns_exists = False

    if auth.is_authenticated:
        if auth.auth_source != AuthSource.CERT:
            raise HTTPException(
                status_code=401,
                detail=('When used with credentials, this API requires '
                        'authentication with TLS client cert'))

        if auth.id_type != IdType.SERVICE:
            raise HTTPException(
                status_code=401,
                detail='A TLS cert of a service is required for this API')
        if auth.service_id != entity_id.service_id:
            raise HTTPException(
                status_code=403,
                detail=(
                    f'Client auth for service id {auth.service_id} does not '
                    f'match CSR for service_id {entity_id.service_id}'))
    else:
        if dns_exists:
            raise HTTPException(
                status_code=403,
                detail=(
                    'CSR is for existing service, must use TLS Client cert '
                    'for authentication'))
    # End of Authorization

    # The Network Services CA signs the CSRs for Service CAs
    certstore = CertStore(network.services_ca)

    certchain = certstore.sign(csr.csr, IdType.SERVICE_CA, request.client.host)

    # Create the service and add it to the network
    service = Service(network=network, service_id=entity_id.service_id)
    network.services[entity_id.service_id] = service

    # Get the certs as strings so we can return them
    signed_cert = certchain.cert_as_string()
    cert_chain = certchain.cert_chain_as_string()

    # We save the public key in the network directory tree. Not sure
    # if we actually need to do this as we can check any cert of the service
    # and its members through the cert chain that is chained to the network
    # root CA
    service.service_ca = ServiceCaSecret(None, service.service_id, network)
    service.service_ca.cert = certchain.signed_cert
    service.service_ca.cert_chain = certchain.cert_chain

    # If someone else already registered a Service then saving the cert will
    # raise an exception
    try:
        await service.service_ca.save(overwrite=False)
    except PermissionError:
        raise HTTPException(409, 'Service CA certificate already exists')

    # We create the DNS entry if it not exists yet to make sure there is no
    # race condition between submitting the CSR through the POST API and
    # registering the service server through the PUT API
    if not dns_exists:
        await dnsdb.create_update(None,
                                  IdType.SERVICE,
                                  auth.remote_addr,
                                  db_session,
                                  service_id=entity_id.service_id)

    data_cert = network.data_secret.cert_as_pem()

    return {
        'signed_cert': signed_cert,
        'cert_chain': cert_chain,
        'network_data_cert_chain': data_cert,
    }
예제 #3
0
async def post_member(request: Request,
                      csr: CertSigningRequestModel,
                      auth: MemberRequestAuthOptionalFast = Depends(
                          MemberRequestAuthOptionalFast)):
    '''
    Submit a Certificate Signing Request for the Member certificate
    and get the cert signed by the Service Members CA
    This API is called by pods
    This API does not require authentication, it needs to be rate
    limited by the reverse proxy (TODO: security)
    '''

    _LOGGER.debug(f'POST Member API called from {request.client.host}')

    await auth.authenticate()

    server: ServiceServer = config.server
    service: Service = server.service
    network: Network = server.network

    # Authorization
    csr_x509: x509 = Secret.csr_from_string(csr.csr)
    common_name = Secret.extract_commonname(csr_x509)

    try:
        entity_id = MembersCaSecret.review_commonname_by_parameters(
            common_name, network.name, service.service_id)
    except PermissionError:
        raise HTTPException(status_code=401,
                            detail=f'Invalid common name {common_name} in CSR')
    except (ValueError, KeyError):
        raise HTTPException(
            status_code=400,
            detail=(
                f'error when reviewing the common name {common_name} in your '
                'CSR'))

    if auth.is_authenticated:
        if auth.auth_source != AuthSource.CERT:
            raise HTTPException(
                status_code=401,
                detail=('When used with credentials, this API requires '
                        'authentication with a TLS client cert'))

        if entity_id.id_type != IdType.MEMBER:
            raise HTTPException(
                status_code=403,
                detail='A TLS cert of a member must be used with this API')

        _LOGGER.debug(f'Signing csr for existing member {entity_id.id}')
    else:
        # TODO: security: consider tracking member UUIDs to avoid
        # race condition between CSR signature and member registration
        # with the Directory server
        ips = server.dns_resolver.resolve(common_name)
        if ips:
            _LOGGER.debug('Attempt to submit CSR for existing member without '
                          'authentication')
            raise HTTPException(
                status_code=401,
                detail=(
                    'Must use TLS client cert when renewing a member cert'))
        _LOGGER.debug(f'Signing csr for new member {entity_id.id}')
    # End of Authorization

    if entity_id.service_id is None:
        raise ValueError(f'No service id found in common name {common_name}')

    if entity_id.service_id != service.service_id:
        raise HTTPException(
            404, f'Incorrect service_id in common name {common_name}')

    # The Network Services CA signs the CSRs for Service CAs
    certstore = CertStore(service.members_ca)

    certchain = certstore.sign(csr.csr, IdType.MEMBER, request.client.host)

    # Get the certs as strings so we can return them
    signed_cert = certchain.cert_as_string()
    cert_chain = certchain.cert_chain_as_string()

    service_data_cert_chain = service.data_secret.cert_as_pem()

    _LOGGER.info(f'Signed certificate with commonname {common_name}')

    config.server.member_db.add_meta(entity_id.id, request.client.host, None,
                                     cert_chain, MemberStatus.SIGNED)

    return {
        'signed_cert': signed_cert,
        'cert_chain': cert_chain,
        'service_data_cert_chain': service_data_cert_chain,
    }