コード例 #1
0
class Controller:
    """
    NDN IoT Controller.

    :ivar app: the python-ndn app
    :ivar system_prefix: a string representing the home namespace
    :ivar system_anchor: a TLV format NDN certificate
    :ivar db: the database handler
    :ivar device_list: the list of device
    :ivar access_list: the list of access rights
    :ivar shared_secret_list: the list of already-shared secrets
    """
    def __init__(self, emit_func):
        self.newly_pub_command = None
        self.newly_pub_payload = None
        self.wait_fetch_cmd_event = None
        self.emit = emit_func
        self.running = True
        self.listen_to_boot_request = False
        self.listen_to_cert_request = False
        self.boot_state = None
        self.boot_event = None

        self.app = NDNApp()
        self.system_prefix = None
        self.system_anchor = None
        self.db = None
        self.device_list = DeviceList()
        self.service_list = ServiceList()
        self.access_list = AccessList()
        self.shared_secret_list = SharedSecrets()

    def save_db(self):
        """
        Save the state into the database.
        """
        logging.debug('Save state to DB')
        if self.db:
            wb = self.db.write_batch()
            logging.debug(self.shared_secret_list.encode())
            wb.put(b'device_list', self.device_list.encode())
            wb.put(b'service_list', self.service_list.encode())
            wb.put(b'access_list', self.access_list.encode())
            wb.put(b'shared_secret_list', self.shared_secret_list.encode())
            wb.write()
            self.db.close()

    def system_init(self):
        """
        Init the system in terms of:
        Step 1: Create/load system prefix and system anchor from the storage if any
        Step 2: Create/load device list, service list, access rights, and shared secrets from the storage
        """

        logging.info("Server starts its initialization")
        # create or get existing state
        # Step One: Meta Info
        # 1. get system prefix from storage (from Level DB)
        import os
        db_dir = os.path.expanduser('~/.ndn-iot-controller/')
        if not os.path.exists(db_dir):
            os.makedirs(db_dir)
        self.db = plyvel.DB(db_dir, create_if_missing=True)
        ret = self.db.get(b'system_prefix')
        if ret:
            logging.info('Found system prefix from db')
            self.system_prefix = ret.decode()
        else:
            self.system_prefix = default_prefix
            self.db.put(b'system_prefix', default_prefix.encode())
        # 2. get system root anchor certificate and private key (from keychain)
        anchor_identity = self.app.keychain.touch_identity(self.system_prefix)
        anchor_key = anchor_identity.default_key()
        self.system_anchor = anchor_key.default_cert().data
        logging.info("Server finishes the step 1 initialization")

        # Step Two: App Layer Support (from Level DB)
        # 1. DEVICES: get all the certificates for devices from storage
        ret = self.db.get(b'device_list')
        if ret:
            logging.info('Found device list from db')
            self.device_list = DeviceList.parse(ret)
        # 2. SERVICES: get service list and corresponding providers
        ret = self.db.get(b'service_list')
        if ret:
            logging.info('Found service list from db')
            self.service_list = ServiceList.parse(ret)
        # 3. ACCESS CONTROL: get all the encryption/decryption key pairs
        ret = self.db.get(b'access_list')
        if ret:
            logging.info('Found access list from db')
            self.access_list = AccessList.parse(ret)
        # 4. SHARED SECRETS: get all shared secrets
        ret = self.db.get(b'shared_secret_list')
        if ret:
            logging.info('Found shared secret from db')
            self.shared_secret_list = SharedSecrets.parse(ret)
        logging.info("Server finishes the step 2 initialization")

    async def iot_connectivity_init(self):
        """
        Init the system in terms of:
        Step 3: Configure network interface, forwarding strategy, and route
        """

        # Step Three: Configure Face and Route
        # 1. Find/create NFD's UDP Multicast Face, BLE Multicast Face, etc.
        face_id = await query_face_id(self.app, default_udp_multi_uri)
        if not face_id:
            logging.fatal("Cannot find existing udp multicast face")
            return
        logging.info("Successfully found UDP multicast face: %d", face_id)
        # 2. Set up NFD's route from IoT system prefix to multicast faces
        ret = await add_route(self.app, self.system_prefix, face_id)
        if ret is True:
            logging.info("Successfully add route.")
        else:
            logging.fatal("Cannot set up the route for IoT prefix")
        # 3. Set up NFD's multicast strategy for IoT system namespace
        ret = await set_strategy(self.app, self.system_prefix,
                                 "/localhost/nfd/strategy/multicast")
        if ret is True:
            logging.info("Successfully add multicast strategy.")
            logging.info("Server finishes the step 3 initialization")
        else:
            logging.fatal("Cannot set up the strategy for IoT prefix")

        @self.app.route('/ndn/sign-on',
                        validator=self.verify_device_sign_on_ecdsa_signature)
        def on_sign_on_interest(name: FormalName, param: InterestParam,
                                app_param: Optional[BinaryStr]):
            """
            OnInterest callback when there is a security bootstrapping request

            :param name: Interest packet name
            :param param: Interest parameters
            :app_param: Interest application paramters

            TODO:Verifying the signature
            """
            if not self.listen_to_boot_request:
                return
            self.process_sign_on_request(name, app_param)

        await asyncio.sleep(0.1)

        @self.app.route(self.system_prefix + '/cert',
                        validator=self.verify_device_sign_on_ecdsa_signature)
        def on_cert_request_interest(name: FormalName, param: InterestParam,
                                     app_param: Optional[BinaryStr]):
            """
            OnInterest callback when there is a certificate request during bootstrapping

            :param name: Interest packet name
            :param param: Interest parameters
            :app_param: Interest application paramters

            TODO:Verifying the signature
            """
            if not self.listen_to_cert_request:
                return
            self.process_cert_request(name, app_param)

        await asyncio.sleep(0.1)

        @self.app.route([
            self.system_prefix,
            bytearray(b'\x08\x01\x01'),
            bytearray(b'\x08\x01\x00')
        ],
                        validator=self.verify_device_ecdsa_signature)
        def on_sd_adv_interest(name: FormalName, param: InterestParam,
                               app_param: Optional[BinaryStr]):
            """
            OnInterest callback when there is an service advertisement

            :param name: Interest packet name
            :param param: Interest parameters
            :app_param: Interest application paramters

            Packet format: prefix = /<home-prefix>/<SD=1>/<ADV=0>/device-id
            App Parameter format:
            TODO:Verifying the signature
            """
            locator = name[3:-1]
            logging.debug("Adv Interest sender locator: %s",
                          Name.to_str(locator))
            fresh_period = struct.unpack("!I", app_param[:4])[0]
            logging.debug("Adv Interest freshness: %s", str(fresh_period))
            service_ids = [sid for sid in app_param[4:]]
            logging.debug('service ids %s', str(service_ids))
            cur_time = self.get_time_now_ms()
            for sid in service_ids:
                # Name format: /<home-prefix>/<service>/<locator>
                sname = [self.system_prefix, b'\x08\x01' + bytes([sid])
                         ] + locator
                sname = Name.to_str(sname)
                logging.debug('Service Name: %s', sname)

                already_added = False
                for item in self.service_list.services:
                    if Name.to_str(item.service_name) == sname:
                        already_added = True
                        item.exp_time = cur_time + fresh_period
                if not already_added:
                    service = ServiceItem()
                    service.service_name = sname
                    service.exp_time = cur_time + fresh_period
                    service.service_id = sid
                    logging.debug('Add new service into the service list')
                    self.service_list.services.append(service)

                already_added = False
                for service_meta in self.service_list.service_meta_items:
                    if service_meta.service_id == sid:
                        already_added = True
                if not already_added:
                    service_meta = ServiceMetaItem()
                    service_meta.service_id = sid
                    aes_key = urandom(16)
                    service_meta.encryption_key = aes_key
                    logging.debug('Add new service meta into the service list')
                    logging.debug('AES key: ')
                    self.service_list.service_meta_items.append(service_meta)

        await asyncio.sleep(0.1)

        @self.app.route([
            self.system_prefix,
            bytearray(b'\x08\x01\x02'),
            bytearray(b'\x08\x01\x00')
        ],
                        validator=self.verify_device_ecdsa_signature)
        def on_sd_ctl_interest(name: FormalName, param: InterestParam,
                               app_param: Optional[BinaryStr]):
            """
            OnInterest callback when device want to query the existing services in the system

            :param name: Interest packet name
            :param param: Interest parameters
            :app_param: Interest application paramters
            TODO:Verifying the signature
            """
            logging.info("Service query from device")
            if app_param is None:
                logging.error("Malformed Interest")
                return
            interested_ids = {sid for sid in app_param}
            result = b''
            cur_time = self.get_time_now_ms()
            for service in self.service_list.services:
                if service.service_id not in interested_ids:
                    continue
                if service.exp_time > cur_time:
                    result += Name.encode(service.service_name)
                    result += struct.pack("i", service.exp_time - cur_time)

            if len(result) > 0:
                self.app.put_data(name,
                                  result,
                                  freshness_period=3000,
                                  identity=self.system_prefix)
                logging.debug("Replied service data back to the device")
            else:
                logging.debug(
                    "Don't have services needed by the device, won't reply")

        await asyncio.sleep(0.1)

        @self.app.route([
            self.system_prefix,
            bytearray(b'\x08\x01\x03'),
            bytearray(b'\x08\x01\x00')
        ],
                        validator=self.verify_device_ecdsa_signature)
        def on_access_control_ekey_request(name: FormalName,
                                           param: InterestParam,
                                           app_param: Optional[BinaryStr]):
            target_service = name[-2]
            logging.debug(bytes(target_service))
            target_service = bytes(target_service)[-1]
            logging.debug('target service id: %s', str(target_service))
            for service_meta in self.service_list.service_meta_items:
                if service_meta.service_id == target_service:
                    # TODO encrypt the content when signature info can be accessed in onInterest callback
                    device = self.device_list.devices[0]
                    # AES encryption
                    iv = urandom(16)
                    cipher = AES.new(bytes(device.aes_key), AES.MODE_CBC, iv)
                    logging.debug(
                        'Use Device AES KEY to encrypt Service AES key')
                    logging.debug('IV:')
                    logging.debug(iv)
                    logging.debug('AES KEY:')
                    logging.debug(bytes(device.aes_key))
                    logging.debug('Service AES Key: ')
                    logging.debug(bytes(service_meta.encryption_key))
                    ct_bytes = cipher.encrypt(
                        bytes(service_meta.encryption_key))
                    content_tlv = CipherBlock()
                    content_tlv.iv = iv
                    content_tlv.cipher = ct_bytes
                    self.app.put_data(name,
                                      content_tlv.encode(),
                                      freshness_period=3000,
                                      identity=self.system_prefix)

        await asyncio.sleep(0.1)

        @self.app.route([
            self.system_prefix,
            bytearray(b'\x08\x01\x03'),
            bytearray(b'\x08\x01\x01')
        ],
                        validator=self.verify_device_ecdsa_signature)
        def on_access_control_dkey_request(name: FormalName,
                                           param: InterestParam,
                                           app_param: Optional[BinaryStr]):
            target_service = name[-2]
            logging.debug(bytes(target_service))
            target_service = bytes(target_service)[-1]
            logging.debug('target service id: %s', str(target_service))
            for service_meta in self.service_list.service_meta_items:
                if service_meta.service_id == target_service:
                    # TODO encrypt the content when signature info can be accessed in onInterest callback
                    device = self.device_list.devices[0]
                    # AES encryption
                    iv = urandom(16)
                    cipher = AES.new(bytes(device.aes_key), AES.MODE_CBC, iv)
                    ct_bytes = cipher.encrypt(
                        bytes(service_meta.encryption_key))
                    content_tlv = CipherBlock()
                    content_tlv.iv = iv
                    content_tlv.cipher = ct_bytes
                    self.app.put_data(name,
                                      content_tlv.encode(),
                                      freshness_period=3000,
                                      identity=self.system_prefix)

    def process_sign_on_request(self, name, app_param):
        """
        Process device's sign on request.

        :param name: Interest packet name
        :param app_param: Interest application parameters
        """
        logging.info("[SIGN ON]: interest received")
        if not app_param:
            logging.error("[SIGN ON]: interest has no parameter")
            return

        m_measure_tp1 = time.time()
        request = SignOnRequest.parse(app_param)
        m_measure_tp2 = time.time()
        logging.debug(
            F'BOOTSTRAPPING-INT1-PKT-DECODING: {m_measure_tp2 - m_measure_tp1}'
        )

        if not request.identifier or not request.capabilities or not request.ecdh_n1:
            logging.error(
                "[SIGN ON]: lack parameters in application parameters")
            return
        self.boot_state['DeviceIdentifier'] = bytes(request.identifier)
        self.boot_state['DeviceCapability'] = bytes(request.capabilities)
        self.boot_state['N1PublicKey'] = bytes(request.ecdh_n1)
        logging.info(self.boot_state)

        shared_secret = None
        for ss in self.shared_secret_list.shared_secrets:
            if bytes(ss.device_identifier) == bytes(request.identifier):
                shared_secret = ss
                break
        if not shared_secret:
            logging.error(
                "[SIGN ON]: no pre-shared information about the device")
            return

        self.boot_state['SharedPublicKey'] = bytes.fromhex(
            bytes(shared_secret.public_key).decode())
        self.boot_state['SharedSymmetricKey'] = bytes.fromhex(
            bytes(shared_secret.symmetric_key).decode())

        # TODO: check whether the device has already bootstrapped
        # TODO: Verify the signature:pre_installed_ecc_key
        logging.info(self.system_anchor)
        m = sha256()
        m.update(self.system_anchor)
        self.boot_state['TrustAnchorDigest'] = m.digest()
        # ECDH

        m_measure_tp1 = time.time()
        ecdh = ECDH()
        m_measure_tp2 = time.time()
        logging.debug(
            F'BOOTSTRAPPING-INT1-ECDH-KEYGEN: {m_measure_tp2 - m_measure_tp1}')

        self.boot_state['N2PrivateKey'] = ecdh.prv_key.to_string()
        self.boot_state['N2PublicKey'] = ecdh.pub_key.to_string()
        # random 16 bytes for salt
        self.boot_state['Salt'] = urandom(16)
        ecdh.encrypt(self.boot_state['N1PublicKey'], self.boot_state['Salt'])
        self.boot_state['SharedAESKey'] = ecdh.derived_key

        response = SignOnResponse()
        response.salt = self.boot_state['Salt']
        response.ecdh_n2 = self.boot_state['N2PublicKey']
        cert_bytes = parse_and_check_tl(self.system_anchor, TypeNumber.DATA)
        response.anchor = cert_bytes
        m_measure_tp2 = time.time()
        logging.info(response.encode())

        m_measure_tp1 = time.time()
        signer = HmacSha256Signer('pre-shared',
                                  self.boot_state['SharedSymmetricKey'])
        self.app.put_data(name,
                          response.encode(),
                          freshness_period=3000,
                          signer=signer)
        m_measure_tp2 = time.time()
        logging.debug(
            F'BOOTSTRAPPING-DATA1-HMAC+ENCODING: {m_measure_tp2 - m_measure_tp1}'
        )

        self.listen_to_cert_request = True

    def process_cert_request(self, name, app_param):
        logging.info("[CERT REQ]: interest received")
        logging.info(name)
        if not app_param:
            logging.error("[CERT REQ]: interest has no parameter")
            return
        request = CertRequest.parse(app_param)
        if not request.identifier or not request.ecdh_n2 or not request.anchor_digest or not request.ecdh_n1:
            raise KeyError(
                "[CERT REQ]: lacking parameters in application parameters")
        logging.info(bytes(request.identifier))
        logging.info(bytes(request.ecdh_n2))
        logging.info(bytes(request.anchor_digest))
        logging.info(bytes(request.ecdh_n1))
        if bytes(request.identifier) != self.boot_state['DeviceIdentifier'] or \
                bytes(request.ecdh_n2) != self.boot_state['N2PublicKey'] or \
                bytes(request.anchor_digest) != self.boot_state['TrustAnchorDigest'] or \
                bytes(request.ecdh_n1) != self.boot_state['N1PublicKey']:
            logging.error("[CERT REQ]: unauthenticated request")
            return
        # anchor signed certificate
        # create identity and key for the device
        # TODO Remove hardcoded livingroom and ask user for which room the device belongs to
        m_measure_tp1 = time.time()
        device_name = '/' + self.system_prefix + '/livingroom' + '/' + bytes(
            request.identifier).decode()
        device_key = self.app.keychain.touch_identity(
            device_name).default_key()
        private_key = get_prv_key_from_safe_bag(device_name)
        default_cert = device_key.default_cert().data
        m_measure_tp2 = time.time()
        logging.debug(
            F'BOOTSTRAPPING-DATA2-ECDSA-KENGEN: {m_measure_tp2 - m_measure_tp1}'
        )

        # re-sign certificate using anchor's key
        cert = parse_certificate(default_cert)
        new_cert_name = cert.name[:-2]
        logging.debug(new_cert_name)
        new_cert_name.append('home')
        new_cert_name.append(
            Name.Component.from_version(SystemRandom().randint(
                10000000, 99999999)))
        logging.debug(new_cert_name)

        m_measure_tp1 = time.time()
        cert = self.app.prepare_data(new_cert_name,
                                     cert.content,
                                     identity=self.system_prefix)
        m_measure_tp2 = time.time()
        logging.debug(
            F'BOOTSTRAPPING-DATA2-ECDSA-RESIGN: {m_measure_tp2 - m_measure_tp1}'
        )

        m_measure_tp1 = time.time()
        # AES
        iv = urandom(16)
        cipher = AES.new(self.boot_state['SharedAESKey'], AES.MODE_CBC, iv)
        ct_bytes = cipher.encrypt(private_key)
        m_measure_tp2 = time.time()
        logging.debug(
            F'BOOTSTRAPPING-DATA2-AES-ENC: {m_measure_tp2 - m_measure_tp1}')

        logging.info('raw private key')
        logging.info(private_key)
        logging.info('Symmetric Key')
        logging.info(self.boot_state['SharedAESKey'])
        # AES IV
        logging.info("IV:")
        logging.info(iv)
        logging.info("Cipher:")
        logging.info(ct_bytes)
        logging.info('Cipher length: ' + str(len(ct_bytes)))
        # encrypted device private key with temporary symmetric key
        # ct = b64encode(ct_bytes)

        response = CertResponse()
        response.cipher = ct_bytes
        response.iv = iv
        cert_bytes = parse_and_check_tl(cert, TypeNumber.DATA)
        response.id_cert = cert_bytes

        m_measure_tp1 = time.time()
        signer = HmacSha256Signer('pre-shared',
                                  self.boot_state['SharedSymmetricKey'])
        self.app.put_data(name,
                          response.encode(),
                          freshness_period=3000,
                          signer=signer)
        m_measure_tp2 = time.time()
        logging.debug(
            F'BOOTSTRAPPING-DATA2-ECDSA+ENCODING: {m_measure_tp2 - m_measure_tp1}'
        )
        self.boot_state["DeviceIdentityName"] = device_name
        self.boot_state['Success'] = True
        self.boot_event.set()
        # TODO: Publish certificates to repo, one cert for each service

    async def bootstrapping(self):
        self.boot_state = {
            'DeviceIdentifier': None,
            'DeviceCapability': None,
            'N1PublicKey': None,
            'N2PrivateKey': None,
            'N2PublicKey': None,
            'SharedAESKey': None,
            'Salt': None,
            'TrustAnchorDigest': None,
            'SharedPublicKey': None,
            'SharedSymmetricKey': None,
            'DeviceIdentityName': None,
            'Success': False
        }
        self.boot_event = asyncio.Event()
        self.listen_to_boot_request = True
        try:
            await asyncio.wait_for(self.boot_event.wait(), timeout=8.0)
        except asyncio.TimeoutError:
            self.boot_event.set()
        if self.boot_state['Success']:
            new_device = DeviceItem()
            new_device.device_id = self.boot_state["DeviceIdentifier"]
            new_device.device_info = self.boot_state["DeviceCapability"]
            new_device.device_identity_name = self.boot_state[
                "DeviceIdentityName"]
            new_device.aes_key = self.boot_state['SharedAESKey']
            self.device_list.devices = [
                device for device in self.device_list.devices if
                bytes(device.device_id) != self.boot_state["DeviceIdentifier"]
            ]
            self.device_list.devices.append(new_device)
            return {
                'st_code': 200,
                'device_id': self.boot_state['DeviceIdentityName']
            }
        await asyncio.sleep(1)
        self.boot_event = None
        self.listen_to_boot_request = False
        self.listen_to_cert_request = False
        return {'st_code': 500}

    async def verify_device_ecdsa_signature(self, name: FormalName,
                                            sig: SignaturePtrs) -> bool:
        sig_info = sig.signature_info
        covered_part = sig.signature_covered_part
        sig_value = sig.signature_value_buf
        if not sig_info or sig_info.signature_type != SignatureType.SHA256_WITH_ECDSA:
            return False
        if not covered_part or not sig_value:
            return False
        identity = [sig_info.key_locator.name[0]
                    ] + sig_info.key_locator.name[-4:-2]
        logging.debug('Extract identity id from key id: %s',
                      Name.to_str(identity))
        key_bits = None
        try:
            key_bits = self.app.keychain.get(identity).default_key().key_bits
        except (KeyError, AttributeError):
            logging.error('Cannot find pub key from keychain')
            return False
        pk = ECC.import_key(key_bits)
        verifier = DSS.new(pk, 'fips-186-3', 'der')
        sha256_hash = SHA256.new()
        for blk in covered_part:
            sha256_hash.update(blk)
        logging.debug(bytes(sig_value))
        logging.debug(len(bytes(sig_value)))
        try:
            verifier.verify(sha256_hash, bytes(sig_value))
        except ValueError:
            return False
        return True

    async def verify_device_sign_on_ecdsa_signature(
            self, name: FormalName, sig: SignaturePtrs) -> bool:
        sig_info = sig.signature_info
        covered_part = sig.signature_covered_part
        sig_value = sig.signature_value_buf
        if not sig_info or sig_info.signature_type != SignatureType.SHA256_WITH_ECDSA:
            return False
        if not covered_part or not sig_value:
            return False

        device_identifier = Name.to_str([sig_info.key_locator.name[0]])[1:]
        logging.debug('Extract device id from key locator: %s',
                      device_identifier)
        pk = None
        for ss in self.shared_secret_list.shared_secrets:
            if bytes(ss.device_identifier) == device_identifier.encode():
                pub_key_bytes = bytes.fromhex(bytes(ss.public_key).decode())
                pk = ECC.construct(curve='p256',
                                   point_x=int.from_bytes(pub_key_bytes[:32],
                                                          byteorder='big'),
                                   point_y=int.from_bytes(pub_key_bytes[32:],
                                                          byteorder='big'))
                # pk = ECC.import_key(bytes(ss.public_key))
                break
        if not pk:
            logging.error(
                "[SIGN ON]: no pre-shared public key about the device")
            return False
        verifier = DSS.new(pk, 'fips-186-3', 'der')
        sha256_hash = SHA256.new()
        for blk in covered_part:
            sha256_hash.update(blk)
        logging.debug(bytes(sig_value))
        logging.debug(len(bytes(sig_value)))
        try:
            verifier.verify(sha256_hash, bytes(sig_value))
        except ValueError:
            return False
        return True

    async def use_service(self, service: str, is_cmd: str, name_or_cmd: str,
                          param: str):
        """
        Use NDN-LITE service
            cmd interest name: /home/SERVICE/CMD/room/device-id/command
            notification interest name: /home/SERVICE/NOTIFY/CMD/room/device-id/command
            data fetching name: /home/SERVICE/DATA/room/device-id
        :param service: full service name, in the format of /home/SERVICE/room/device-id
        :param is_cmd: whether to send a command to the service
        :param name_or_cmd: the command id (if is_command is true) or the content-id (if is_cmd is false)
        :param param: content payload or command parameters
        :return: a dict object containing the state
        """
        service_name = Name.from_str(service)
        service_id = service_name[1][2]
        logging.debug(f'Use service: {str(service_id)}')
        encryption_key = None
        for service_meta in self.service_list.service_meta_items:
            if service_meta.service_id == service_id:
                encryption_key = service_meta.encryption_key
        if is_cmd == 'true':
            logging.debug(
                F'******Command publish timestamp: {int(round(time.time() * 1000))}'
            )
            service_name.insert(2, 'CMD')
            service_name = service_name + Name.from_str(name_or_cmd)
            service_name.append(Component.from_timestamp(timestamp()))
            notification_name = service_name[:]
            notification_name.insert(2, 'NOTIFY')

            need_register = False
            need_unregister = False
            if self.newly_pub_command is None:
                need_register = True
            elif Name.to_str(self.newly_pub_command[:3]) != Name.to_str(
                    service_name[:3]):
                need_register = True
                need_unregister = True

            if need_unregister:
                success = await self.app.unregister(self.newly_pub_command[:3])
                if not success:
                    logging.debug(
                        'cannot unregister prefix for command publish')

            self.newly_pub_command = service_name
            self.newly_pub_payload = param.encode()
            logging.debug(f'New pub info: {param}')
            logging.debug('Encryption Key: ')
            logging.debug(bytes(encryption_key))
            logging.debug('Plaintext: ')
            logging.debug(self.newly_pub_payload)
            # AES encryption
            iv = urandom(16)
            cipher = AES.new(bytes(encryption_key), AES.MODE_CBC, iv)
            ct_bytes = cipher.encrypt(pad(self.newly_pub_payload, 16))
            content_tlv = CipherBlock()
            content_tlv.iv = iv
            content_tlv.cipher = ct_bytes
            self.newly_pub_payload = content_tlv.encode()

            logging.info(
                F'Publish new content Data packet {Name.to_str(self.newly_pub_command)}'
            )

            if need_register:

                def on_service_fetch(name: FormalName,
                                     int_param: InterestParam,
                                     app_param: Optional[BinaryStr]):
                    logging.debug(
                        'received Interest to fetch newly published command')
                    self.app.put_data(self.newly_pub_command,
                                      self.newly_pub_payload,
                                      freshness_period=3000,
                                      identity=self.system_prefix)
                    return

                success = await self.app.register(service_name[:3],
                                                  on_service_fetch)
                if not success:
                    logging.debug('cannot register prefix for command publish')

            coroutine = self.app.express_interest(notification_name,
                                                  must_be_fresh=True,
                                                  can_be_prefix=True,
                                                  identity=self.system_prefix)
            ret = {
                'name': Name.to_str(service_name),
                'response_type': 'CommandPublished'
            }
        else:
            service_name.insert(2, 'DATA')
            service_name = service_name + Name.from_str(name_or_cmd)
            time1 = time.time()
            ret = await self.express_interest(service_name, None, True, True,
                                              False)
            time2 = time.time()
            logging.debug(
                F'******Data Fetching Round Trip Time: {time2 - time1}s******')
            if ret['response_type'] == 'Data':
                content = ret['content']
                content = CipherBlock.parse(content)
                iv = bytes(content.iv)
                cipher = AES.new(bytes(encryption_key), AES.MODE_CBC, iv)
                payload = cipher.decrypt(bytes(content.cipher))

                time2 = time.time()
                logging.debug(
                    F'******Data Fetching Finish Time: {time2 - time1}s******')

                payload = unpad(payload, 16)
                ret['content'] = payload.decode()
        return ret

    async def manage_policy_add(self, device_name: str, data_name: str,
                                key_name: str, policy_name: str):
        interest_name = Name.from_str(device_name)
        interest_name.insert(1, 'POLICY')
        interest_name = interest_name + Name.from_str(policy_name)
        param = PolicyAddRequest()
        param.data_name = data_name.encode()
        param.key_name = key_name.encode()
        time1 = time.time()
        ret = await self.express_interest(interest_name, param.encode(), True,
                                          True, True)
        time2 = time.time()
        logging.debug(
            F'******Policy Update Round Trip Time: {time2 - time1}s******')
        return ret

    async def manage_policy_remove(self, policy_to_del):
        pass

    async def express_interest(self, name, app_param, be_fresh: bool,
                               be_prefix: bool, need_sig: bool):
        ret = {'name': Name.to_str(name)}
        try:
            if need_sig:
                data_name, meta_info, content = await self.app.express_interest(
                    name,
                    app_param,
                    must_be_fresh=be_fresh,
                    can_be_prefix=be_prefix,
                    identity=self.system_prefix,
                    validator=self.verify_device_ecdsa_signature)
            else:
                data_name, meta_info, content = await self.app.express_interest(
                    name,
                    app_param,
                    must_be_fresh=be_fresh,
                    can_be_prefix=be_prefix)

        except InterestNack as e:
            ret['response_type'] = 'NetworkNack'
            ret['reason'] = e.reason
        except InterestTimeout:
            ret['response_type'] = 'Timeout'
        else:
            ret['response_type'] = 'Data'
            ret['name'] = Name.to_str(data_name)
            ret['freshness_period'] = meta_info.freshness_period
            ret['content_type'] = meta_info.content_type
            ret['content'] = content
        return ret

    async def run(self):
        logging.info("Restarting app...")
        while True:
            try:
                await self.app.main_loop(self.iot_connectivity_init())
            except KeyboardInterrupt:
                logging.info('Receiving Ctrl+C, shutdown')
                break
            except (FileNotFoundError, ConnectionRefusedError):
                logging.info("NFD disconnected...")
            finally:
                self.app.shutdown()
            await asyncio.sleep(3.0)

    ###################
    @staticmethod
    def get_time_now_ms():
        return round(time.time() * 1000.0)

    def on_register_failed(self, prefix):
        logging.fatal("Prefix registration failed: %s", prefix)

    def setup_sd(self):
        # /<home-prefix>/<SD=1>
        sd_prefix = Name(self.system_prefix).append(
            Name.Component.fromNumber(1))
        self.face.registerPrefix(sd_prefix, None, self.on_register_failed)
        # /<home-prefix>/<SD=1>/<SD_ADV=0>
        self.face.setInterestFilter(
            Name(sd_prefix).append(Name.Component.fromNumber(0)),
            self.on_sd_adv_interest)
        # /<home-prefix>/<SD_CTL=2>
        sd_ctl_prefix = Name(self.system_prefix).append(
            Name.Component.fromNumber(2))
        self.face.registerPrefix(sd_ctl_prefix, None, self.on_register_failed)
        # /<home-prefix>/<SD_CTL=2>/<SD_CTL_META=0>
        self.face.setInterestFilter(
            Name(sd_ctl_prefix).append(Name.Component.fromNumber(0)),
            self.on_sd_ctl_interest)
コード例 #2
0
class Controller:
    def __init__(self, emit_func):
        self.emit = emit_func
        self.running = True
        self.networking_ready = False
        self.listen_to_boot_request = False
        self.listen_to_cert_request = False
        self.boot_state = None

        self.app = NDNApp()
        self.system_prefix = None
        self.system_anchor = None
        self.db = None
        self.device_list = DeviceList()
        self.service_list = ServiceList()
        self.access_list = AccessList()
        self.shared_secret_list = SharedSecrets()

    def save_db(self):
        logging.debug('Save state to DB')
        if self.db:
            wb = self.db.write_batch()
            logging.debug(self.shared_secret_list.encode())
            wb.put(b'device_list', self.device_list.encode())
            wb.put(b'service_list', self.service_list.encode())
            wb.put(b'access_list', self.access_list.encode())
            wb.put(b'shared_secret_list', self.shared_secret_list.encode())
            wb.write()
            self.db.close()

    def system_init(self):
        logging.info("Server starts its initialization")
        # create or get existing state
        # Step One: Meta Info
        # 1. get system prefix from storage (from Level DB)
        import os
        db_dir = os.path.expanduser('~/.ndn-iot-controller/')
        if not os.path.exists(db_dir):
            os.makedirs(db_dir)
        self.db = plyvel.DB(db_dir, create_if_missing=True)
        ret = self.db.get(b'system_prefix')
        if ret:
            logging.info('Found system prefix from db')
            self.system_prefix = ret.decode()
        else:
            self.system_prefix = default_prefix
            self.db.put(b'system_prefix', default_prefix.encode())
        # 2. get system root anchor certificate and private key (from keychain)
        anchor_identity = self.app.keychain.touch_identity(self.system_prefix)
        anchor_key = anchor_identity.default_key()
        self.system_anchor = anchor_key.default_cert().data
        logging.info("Server finishes the step 1 initialization")

        # Step Two: App Layer Support (from Level DB)
        # 1. DEVICES: get all the certificates for devices from storage
        ret = self.db.get(b'device_list')
        if ret:
            logging.info('Found device list from db')
            self.device_list = DeviceList.parse(ret)
        # 2. SERVICES: get service list and corresponding providers
        ret = self.db.get(b'service_list')
        if ret:
            logging.info('Found service list from db')
            self.service_list = ServiceList.parse(ret)
        # 3. ACCESS CONTROL: get all the encryption/decryption key pairs
        ret = self.db.get(b'access_list')
        if ret:
            logging.info('Found access list from db')
            self.access_list = AccessList.parse(ret)
        # 4. SHARED SECRETS: get all shared secrets
        ret = self.db.get(b'shared_secret_list')
        if ret:
            logging.info('Found shared secret from db')
            self.shared_secret_list = SharedSecrets.parse(ret)
        logging.info("Server finishes the step 2 initialization")

    async def iot_connectivity_init(self):
        # Step Three: Configure Face and Route
        # 1. Find/create NFD's UDP Multicast Face, BLE Multicast Face, etc.
        face_id = await self.query_face_id(default_udp_multi_uri)
        if not face_id:
            logging.fatal("Cannot find existing udp multicast face")
            return
        logging.info("Successfully found UDP multicast face: %d", face_id)
        # 2. Set up NFD's route from IoT system prefix to multicast faces
        ret = await self.add_route(self.system_prefix, face_id)
        if ret is True:
            logging.info("Successfully add route.")
        else:
            logging.fatal("Cannot set up the route for IoT prefix")
        # 3. Set up NFD's multicast strategy for IoT system namespace
        ret = await self.set_strategy(self.system_prefix, "/localhost/nfd/strategy/multicast")
        if ret is True:
            self.networking_ready = True
            logging.info("Successfully add multicast strategy.")
            logging.info("Server finishes the step 3 initialization")
        else:
            logging.fatal("Cannot set up the strategy for IoT prefix")

        @self.app.route('/ndn/sign-on')
        def on_sign_on_interest(name: FormalName, param: InterestParam, app_param: Optional[BinaryStr]):
            # TODO:Verifying the signature
            if not self.listen_to_boot_request:
                return
            self.process_sign_on_request(name, app_param)

        await asyncio.sleep(0.1)

        @self.app.route(self.system_prefix + '/cert')
        def on_cert_request_interest(name: FormalName, param: InterestParam, app_param: Optional[BinaryStr]):
            # TODO:Verifying the signature
            if not self.listen_to_cert_request:
                return
            self.process_cert_request(name, app_param)

        await asyncio.sleep(0.1)

        @self.app.route([self.system_prefix, bytearray(b'\x08\x01\x01'), bytearray(b'\x08\x01\x00')])
        def on_sd_adv_interest(name: FormalName, param: InterestParam, app_param: Optional[BinaryStr]):
            # prefix = /<home-prefix>/<SD=1>/<ADV=0>
            locator = name[3:-1]
            fresh_period = struct.unpack("!I", app_param[:4])[0]
            service_ids = [sid for sid in app_param[4:]]
            logging.debug("ON ADV: %s %s %s", locator, fresh_period, service_ids)
            cur_time = self.get_time_now_ms()
            for sid in service_ids:
                # /<home-prefix>/<SD=1>/<service>/<locator>
                sname = [self.system_prefix, bytearray(b'\x08\x011'), sid, locator]
                sname = Name.normalize(sname)
                logging.debug("SNAME: %s", sname)
                self.real_service_list[sname] = cur_time + fresh_period

        await asyncio.sleep(0.1)

        @self.app.route([self.system_prefix, bytearray(b'\x08\x01\x02'), bytearray(b'\x08\x01\x00')])
        def on_sd_ctl_interest(name: FormalName, param: InterestParam, app_param: Optional[BinaryStr]):
            logging.info("SD : on interest")
            if app_param is None:
                logging.error("Malformed Interest")
                return
            interested_ids = {sid for sid in app_param}
            result = b''
            cur_time = self.get_time_now_ms()
            for sname, exp_time in self.real_service_list.items():
                sid = sname[2][2]
                if sid in interested_ids and exp_time > cur_time:
                    result += Name.encode(sname)
                    result += struct.pack("i", exp_time - cur_time)

            self.app.put_data(name, result, freshness_period=3000, identity=self.system_prefix)
            logging.debug("PutData")
            logging.debug(name)

    def process_sign_on_request(self, name, app_param):
        logging.info("[SIGN ON]: interest received")
        if not app_param:
            logging.error("[SIGN ON]: interest has no parameter")
            return
        state = {'DeviceIdentifier': None,
                  'DeviceCapability': None,
                  'N1PublicKey': None,
                  'N2PrivateKey':None,
                  'N2PublicKey':None,
                  'SharedKey':None,
                  'Salt':None,
                  'TrustAnchorDigest':None,
                  'SharedPublicKey':None,
                  'SharedSymmetricKey':None,
                  'DeviceIdentityName':None}
        request = SignOnRequest.parse(app_param)

        if not request.identifier or not request.capabilities or not request.ecdh_n1:
            logging.error("[SIGN ON]: lack parameters in application parameters")
            return
        state['DeviceIdentifier'] = bytes(request.identifier)
        state['DeviceCapability'] = bytes(request.capabilities)
        state['N1PublicKey'] = bytes(request.ecdh_n1)
        logging.info(state)

        shared_secret = None
        for ss in self.shared_secret_list.shared_secrets:
            if bytes(ss.device_identifier) == bytes(request.identifier):
                shared_secret = ss
                break
        if not shared_secret:
            logging.error("[SIGN ON]: no pre-shared information about the device")
            return

        state['SharedPublicKey'] = bytes.fromhex(bytes(shared_secret.public_key).decode())
        state['SharedSymmetricKey'] = bytes.fromhex(bytes(shared_secret.symmetric_key).decode())

        # TODO: check whether the device has already bootstrapped
        # TODO: Verify the signature:pre_installed_ecc_key
        logging.info(self.system_anchor)
        m = sha256()
        m.update(self.system_anchor)
        state['TrustAnchorDigest'] = m.digest()
        # ECDH
        ecdh = ECDH()
        state['N2PrivateKey'] = ecdh.prv_key.to_string()
        state['N2PublicKey'] = ecdh.pub_key.to_string()
        # random 16 bytes for salt
        state['Salt'] = urandom(16)
        ecdh.encrypt(state['N1PublicKey'], state['Salt'])
        state['SharedKey'] = ecdh.derived_key

        response = SignOnResponse()
        response.salt = state['Salt']
        response.ecdh_n2 = state['N2PublicKey']
        cert_bytes = parse_and_check_tl(self.system_anchor, TypeNumber.DATA)
        response.anchor = cert_bytes

        logging.info(response.encode())

        signer = HmacSha256Signer('pre-shared', state['SharedSymmetricKey'])
        self.app.put_data(name, response.encode(), freshness_period=3000, signer=signer)
        self.boot_state = state
        self.listen_to_cert_request = True

    def process_cert_request(self, name, app_param):
        logging.info("[CERT REQ]: interest received")
        logging.info(name)
        if not app_param:
            logging.error("[CERT REQ]: interest has no parameter")
            return
        request = CertRequest.parse(app_param)
        if not request.identifier or not request.ecdh_n2 or not request.anchor_digest or not request.ecdh_n1:
            raise KeyError("[CERT REQ]: lacking parameters in application parameters")
        logging.info(bytes(request.identifier))
        logging.info(bytes(request.ecdh_n2))
        logging.info(bytes(request.anchor_digest))
        logging.info(bytes(request.ecdh_n1))
        if bytes(request.identifier) != self.boot_state['DeviceIdentifier'] or \
                bytes(request.ecdh_n2) != self.boot_state['N2PublicKey'] or \
                bytes(request.anchor_digest) != self.boot_state['TrustAnchorDigest'] or \
                bytes(request.ecdh_n1) != self.boot_state['N1PublicKey']:
            logging.error("[CERT REQ]: unauthenticated request")
            return
        # anchor signed certificate
        # create identity and key for the device
        device_name = self.system_prefix + '/' + bytes(request.identifier).decode()
        device_key = self.app.keychain.touch_identity(device_name).default_key()
        private_key = get_prv_key_from_safe_bag(device_name)
        default_cert = device_key.default_cert().data
        # resign certificate using anchor's key
        cert = parse_certificate(default_cert)
        new_cert_name = cert.name[:-2]
        new_cert_name.append('home')
        new_cert_name.append('001')
        cert = self.app.prepare_data(new_cert_name, cert.content, identity=self.system_prefix)
        # AES
        iv = urandom(16)
        cipher = AES.new(self.boot_state['SharedKey'], AES.MODE_CBC, iv)
        ct_bytes = cipher.encrypt(pad(private_key, AES.block_size))
        logging.info('Symmetic Key')
        logging.info(self.boot_state['SharedKey'])
        # AES IV
        logging.info("IV:")
        logging.info(iv)
        # encrpted device private key with temporary symmetric key
        ct = b64encode(ct_bytes)
        logging.info("Cipher:")
        logging.info(ct)

        response = CertResponse()
        response.cipher = ct
        response.iv = iv
        cert_bytes = parse_and_check_tl(cert, TypeNumber.DATA)
        response.id_cert = cert_bytes

        signer = HmacSha256Signer('pre-shared', self.boot_state['SharedSymmetricKey'])
        self.app.put_data(name, response.encode(), freshness_period=3000, signer=signer)

    async def bootstrapping(self):
        self.listen_to_boot_request = True
        #
        # # TODO: wait for bootstrapping process
        # new_device = self.device_list.device.add()
        # new_device.device_id = self.boot_state["DeviceIdentifier"]
        # new_device.device_info = self.boot_state["DeviceCapability"]
        # new_device.device_cert_name = self.boot_state["DeviceIdentityName"]
        # return {'st_code':200,'device_id': self.boot_state['DeviceIdentifier'].decode('utf-8')}


    def get_access_status(self, parameter_list):
        pass

    def invoke_service(self, parameter_list):
        pass

    async def query_face_id(self, uri):
        query_filter = FaceQueryFilter()
        query_filter.face_query_filter = FaceQueryFilterValue()
        query_filter.face_query_filter.uri = uri.encode('utf-8')
        query_filter_msg = query_filter.encode()
        name = Name.from_str("/localhost/nfd/faces/query") + [Component.from_bytes(query_filter_msg)]
        try:
            _, _, data = await self.app.express_interest(name, lifetime=1000, can_be_prefix=True, must_be_fresh=True)
        except (InterestCanceled, InterestTimeout, InterestNack, ValidationFailure, NetworkError):
             logging.error(f'Query failed')
             return None
        ret = FaceStatusMsg.parse(data)
        logging.info(ret)
        return ret.face_status[0].face_id

    async def add_route(self, name: str, face_id: int):
        interest = make_command('rib', 'register', name=name, face_id=face_id)
        try:
            _, _, data = await self.app.express_interest(interest, lifetime=1000, can_be_prefix=True, must_be_fresh=True)
        except (InterestCanceled, InterestTimeout, InterestNack, ValidationFailure, NetworkError):
            logging.error(f'Command failed')
            return False
        ret = parse_response(data)
        if ret['status_code'] <= 399:
            return True
        return False

    async def remove_route(self, name: str, face_id: int):
        interest = make_command('rib', 'unregister', name=name, face_id=face_id)
        try:
            _, _, data = await self.app.express_interest(interest, lifetime=1000, can_be_prefix=True, must_be_fresh=True)
        except (InterestCanceled, InterestTimeout, InterestNack, ValidationFailure, NetworkError):
            logging.error(f'Command failed')
            return False
        ret = parse_response(data)
        if ret['status_code'] <= 399:
            return True
        return False

    async def set_strategy(self, name: str, strategy: str):
        interest = make_command('strategy-choice', 'set', name=name, strategy=strategy)
        try:
            _, _, data = await self.app.express_interest(interest, lifetime=1000, can_be_prefix=True, must_be_fresh=True)
        except (InterestCanceled, InterestTimeout, InterestNack, ValidationFailure, NetworkError):
            logging.error(f'Command failed')
            return False
        ret = parse_response(data)
        if ret['status_code'] <= 399:
            return True
        return False

    async def unset_strategy(self, name: str):
        interest = make_command('strategy-choice', 'unset', name=name)
        try:
            _, _, data = await self.app.express_interest(interest, lifetime=1000, can_be_prefix=True, must_be_fresh=True)
        except (InterestCanceled, InterestTimeout, InterestNack, ValidationFailure, NetworkError):
            logging.error(f'Command failed')
            return False
        ret = parse_response(data)
        if ret['status_code'] <= 399:
            return True
        return False

    async def run(self):
        logging.info("Restarting app...")
        while True:
            try:
                await self.app.main_loop(self.iot_connectivity_init())
            except KeyboardInterrupt:
                logging.info('Receiving Ctrl+C, shutdown')
                break
            except (FileNotFoundError, ConnectionRefusedError):
                logging.info("NFD disconnected...")
            finally:
                self.app.shutdown()
            await asyncio.sleep(3.0)

    ###################
    @staticmethod
    def get_time_now_ms():
        return round(time.time() * 1000.0)

    def on_register_failed(self, prefix):
        logging.fatal("Prefix registration failed: %s", prefix)

    def setup_sd(self):
        # /<home-prefix>/<SD=1>
        sd_prefix = Name(self.system_prefix).append(Name.Component.fromNumber(1))
        self.face.registerPrefix(sd_prefix, None, self.on_register_failed)
        # /<home-prefix>/<SD=1>/<SD_ADV=0>
        self.face.setInterestFilter(Name(sd_prefix).append(Name.Component.fromNumber(0)), self.on_sd_adv_interest)
        # /<home-prefix>/<SD_CTL=2>
        sd_ctl_prefix = Name(self.system_prefix).append(Name.Component.fromNumber(2))
        self.face.registerPrefix(sd_ctl_prefix, None, self.on_register_failed)
        # /<home-prefix>/<SD_CTL=2>/<SD_CTL_META=0>
        self.face.setInterestFilter(Name(sd_ctl_prefix).append(Name.Component.fromNumber(0)), self.on_sd_ctl_interest)

    def get_service_list(self):
        ret = ServiceList()
        for sname, exp_time in self.real_service_list.items():
            item = ServiceItem()
            item.service_id = Name(sname)[2].toNumber()
            item.service_name = sname
            item.exp_time = exp_time
            ret.service.append(item)
        return ret

    def set_service_list(self, srv_lst):
        self.real_service_list = {}
        cur_time = self.get_time_now_ms()
        for item in srv_lst.service:
            if item.exp_time > cur_time:
                self.real_service_list[item.service_name] = item.exp_time

    service_list = property(get_service_list, set_service_list)