예제 #1
0
    def _recv_bundle(self, ctr):
        if not self._recv_for(ctr, self._config.node_id):
            return

        rec = cbor2.loads(ctr.block_num(1).getfieldval('btsd'))
        LOGGER.info('Record RX: %s', encode_diagnostic(rec))
        if not isinstance(rec, List):
            raise ValueError('Administrative record is not a list type, got %s', type(rec))
        rec_type = int(rec[0])
        handler = self._rec_type_map[rec_type]

        handler(ctr, rec[1])

        return True
예제 #2
0
    def send_bundle(self, ctr):
        ''' Perform agent handling to send a bundle.
        Part of this is to update final CRCs on all blocks and
        assign block numbers.

        :param ctr: The bundle container to send.
        :type ctr: :py:cls:`BundleContainer`
        '''
        ctr.reload()
        self._apply_primary(ctr)
        ctr.fix_block_num()
        ctr.bundle.fill_fields()

        for step in self._tx_chain:
            self._logger.debug('Performing TX step %5.1f: %s', step.order,
                               step.name)
            try:
                if step.action(ctr):
                    self._logger.debug('Step %5.1f interrupted the chain',
                                       step.order)
                    break
            except Exception as err:
                self._logger.error('Step %5.1f failed with exception: %s',
                                   step.order, err)
                break

        if ctr.route and not ctr.sender:
            # Assume the route is a TxRouteItem
            ctr.sender = self._cl_agent[ctr.route.cl_type].send_bundle_func(
                ctr.route.raw_config)

        if ctr.sender is None:
            raise RuntimeError('TX chain completed with no sender for %s',
                               ctr.log_name())

        ctr.fix_block_num()
        ctr.bundle.fill_fields()
        ctr.bundle.update_all_crc()

        self._logger.debug('Sending bundle\n%s', ctr.bundle.show(dump=True))
        data = bytes(ctr.bundle)
        self._logger.info('send_bundle size %d', len(data))
        self._logger.debug('send_bundle data %s',
                           encode_diagnostic(cbor2.loads(data)))
        ctr.sender(data)
예제 #3
0
    def _send_msg(self, msg, address, local_address):
        ''' Send an NMP message

        :param msg: The message content.
        :param address: The remote address to send to.
        :param local_address: The address to send from.
        '''
        LOGGER.info('Message TX: %s', encode_diagnostic(msg))

        ctr = BundleContainer()
        ctr.bundle.primary = PrimaryBlock(
            bundle_flags=(PrimaryBlock.Flag.NO_FRAGMENT
                          | PrimaryBlock.Flag.REQ_DELIVERY_REPORT),
            destination='dtn:~neighbor',
            crc_type=AbstractBlock.CrcType.CRC32,
        )
        ctr.bundle.blocks = [
            CanonicalBlock(
                type_code=1,
                block_num=1,
                crc_type=AbstractBlock.CrcType.CRC32,
                btsd=cbor2.dumps(msg),
            ),
        ]
        # Force the route
        ctr.route = TxRouteItem(
            eid_pattern=None,
            next_nodeid=ctr.bundle.primary.destination,
            cl_type='udpcl',
            raw_config=dict(
                address=address,
                port=4556,
                local_address=local_address,
            ),
        )

        self._agent.send_bundle(ctr)
예제 #4
0
    def _recv_bundle(self, ctr):
        if not self._recv_for(ctr, 'dtn:~neighbor'):
            return

        msg = cbor2.loads(ctr.block_num(1).getfieldval('btsd'))
        LOGGER.info('Message RX: %s', encode_diagnostic(msg))

        msg_type = msg[MsgKeys.MSG_TYPE]
        if msg_type == MsgType.HELLO:
            pri_blk = ctr.bundle.primary

            node_id = pri_blk.source
            neighbor = self._one_hop.get(node_id)
            if not neighbor:
                neighbor = OneHopNeighbor(node_id=node_id, )
                self._one_hop[node_id] = neighbor

            # at least this much is known, for now
            neighbor.link_status = LinkStatus.HEARD

            valid_ms = msg.get(MsgKeys.HELLO_VALIDITY_TIME)
            if valid_ms is not None and pri_blk.create_ts.dtntime:
                neighbor.valid_until = pri_blk.create_ts.dtntime + datetime.timedelta(
                    milliseconds=valid_ms),

            clset = msg.get(MsgKeys.HELLO_CLSET, [])
            for cldef in clset:
                cltype = cldef.get(ClKeys.CL_TYPE)
                if cltype == ClType.UDPCL:
                    addr_list = cldef.get(ClKeys.ADDR, [])
                    route = TxRouteItem(
                        eid_pattern=re.compile(r''),
                        next_nodeid=node_id,
                        cl_type='udpcl',
                        raw_config=dict(
                            address=str(ipaddress.ip_address(addr_list[0])),
                            port=cldef.get(ClKeys.PORT, 4556),
                        ),
                    )
                elif cltype == ClType.TCPCL:
                    addr_list = cldef.get(ClKeys.ADDR, [])
                    route = TxRouteItem(
                        eid_pattern=re.compile(r''),
                        next_nodeid=node_id,
                        cl_type='tcpcl',
                        raw_config=dict(
                            address=str(ipaddress.ip_address(addr_list[0])),
                            port=cldef.get(ClKeys.PORT, 4556),
                        ),
                    )

                LOGGER.info('CL %s to route %s', cldef, route)
                if route:
                    neighbor.tx_routes.append(route)

            peerset = msg.get(MsgKeys.HELLO_PEERSET, [])
            for peer in peerset:
                (peer_nodeid, peer_link_status) = peer
                if (self._config.node_id == peer_nodeid and peer_link_status
                        in (LinkStatus.HEARD, LinkStatus.SYMMETRIC)):
                    neighbor.link_status = LinkStatus.SYMMETRIC

            LOGGER.info('HELLO from: %s', neighbor)

        else:
            LOGGER.warning('Ignoring unknown NMP message type %s', msg_type)

        return True
예제 #5
0
    def verify_bib(self, ctr, bib):
        addl_protected = b''
        addl_unprotected = {}
        aad_scope = 0x7
        for param in bib.payload.parameters:
            if param.type_code == 3:
                addl_protected = bytes(param.value)
            elif param.type_code == 4:
                addl_unprotected = dict(param.value)
            elif param.type_code == 5:
                aad_scope = int(param.value)

        addl_protected_map = cbor2.loads(
            addl_protected) if addl_protected else {}
        dupe_keys = set(addl_protected_map.keys()).intersection(
            set(addl_unprotected.keys()))
        if dupe_keys:
            LOGGER.warning('Duplicate keys in additional headers: %s',
                           dupe_keys)
            return StatusReport.ReasonCode.FAILED_SEC
        addl_headers = dict(addl_protected_map)
        addl_headers.update(addl_unprotected)

        bundle_at = DtnTimeField.dtntime_to_datetime(
            ctr.bundle.primary.create_ts.getfieldval('dtntime'))
        val_ctx = ValidationContext(
            trust_roots=[
                cert.public_bytes(serialization.Encoding.DER)
                for cert in self._ca_certs
            ],
            other_certs=[
                cert.public_bytes(serialization.Encoding.DER)
                for cert in self._cert_chain
            ],
            moment=bundle_at,
        )
        LOGGER.debug('Validating certificates at time %s', bundle_at)

        failure = None
        for (ix, blk_num) in enumerate(bib.payload.targets):
            target_blk = ctr.block_num(blk_num)
            for result in bib.payload.results[ix].results:
                msg_cls = CoseMessage._COSE_MSG_ID[result.type_code]

                # replace detached payload
                msg_enc = bytes(result.getfieldval('value'))
                msg_dec = cbor2.loads(msg_enc)
                LOGGER.debug('Received COSE message\n%s',
                             encode_diagnostic(msg_dec))
                msg_dec[2] = target_blk.getfieldval('btsd')

                msg_obj = msg_cls.from_cose_obj(msg_dec)
                msg_obj.external_aad = CoseContext.get_bpsec_cose_aad(
                    ctr, target_blk, bib, aad_scope, addl_protected)
                # use additional headers as defaults
                for (key, val) in msg_cls._parse_header(addl_headers).items():
                    msg_obj.uhdr.setdefault(key, val)
                LOGGER.info('full uhdr %s', msg_obj.uhdr)

                x5t_item = msg_obj.get_attr(headers.X5t)
                x5t = X5T.decode(x5t_item) if x5t_item else None

                x5chain_item = msg_obj.get_attr(headers.X5chain)
                if isinstance(x5chain_item, bytes):
                    x5chain = [x5chain_item]
                else:
                    x5chain = x5chain_item
                LOGGER.info('Validating X5t %s and X5chain length %d',
                            x5t.encode() if x5t else None,
                            len(x5chain) if x5chain else 0)

                if x5t is None and x5chain:
                    # Only one possible end-entity cert
                    LOGGER.warning('No X5T in header, assuming single chain')
                    found_chain = x5chain
                else:
                    try:
                        found_chain = x5chain if x5t.matches(
                            x5chain[0]) else None
                        if not found_chain:
                            raise RuntimeError(
                                'No chain matcing end-entity cert for {}'.
                                format(x5t.encode()))
                        LOGGER.debug(
                            'Found chain matcing end-entity cert for %s',
                            x5t.encode())
                    except Exception as err:
                        LOGGER.error(
                            'Failed to find cert chain for block num %d: %s',
                            blk_num, err)
                        failure = StatusReport.ReasonCode.FAILED_SEC
                        continue

                LOGGER.debug('Validating chain with %d certs against %d CAs',
                             len(found_chain), len(self._ca_certs))
                try:
                    val = CertificateValidator(
                        end_entity_cert=found_chain[0],
                        intermediate_certs=found_chain[1:],
                        validation_context=val_ctx)
                    val.validate_usage(
                        key_usage={'digital_signature'},
                        extended_key_usage={'1.3.6.1.5.5.7.3.35'},
                        extended_optional=True)
                except Exception as err:
                    LOGGER.error('Failed to verify chain on block num %d: %s',
                                 blk_num, err)
                    failure = StatusReport.ReasonCode.FAILED_SEC
                    continue

                peer_nodeid = bib.payload.source
                end_cert = x509.load_der_x509_certificate(
                    found_chain[0], default_backend())
                authn_nodeid = tcpcl.session.match_id(
                    peer_nodeid, end_cert, x509.UniformResourceIdentifier,
                    LOGGER, 'NODE-ID')
                if not authn_nodeid:
                    LOGGER.error(
                        'Failed to authenticate peer "%s" on block num %d',
                        peer_nodeid, blk_num)
                    failure = StatusReport.ReasonCode.FAILED_SEC
                    # Continue on to verification

                try:
                    msg_obj.key = self.extract_cose_key(end_cert.public_key())
                    msg_obj.verify_signature()
                    LOGGER.info('Verified signature on block num %d', blk_num)
                except Exception as err:
                    LOGGER.error(
                        'Failed to verify signature on block num %d: %s',
                        blk_num, err)
                    failure = StatusReport.ReasonCode.FAILED_SEC

        return failure
예제 #6
0
    def apply_bib(self, ctr):
        if not self._priv_key:
            LOGGER.warning('No private key')
            return

        addl_protected_map = {}
        addl_unprotected = {}
        aad_scope = 0x3

        target_block_nums = [
            blk.block_num for blk in ctr.bundle.blocks
            if blk.type_code in self._config.integrity_for_blocks
        ]
        if not target_block_nums:
            LOGGER.warning('No target blocks have matching type')
            return

        x5chain = []
        for cert in self._cert_chain:
            x5chain.append(cert.public_bytes(serialization.Encoding.DER))
        if self._config.integrity_include_chain:
            addl_protected_map[headers.X5chain.identifier] = X5Chain(
                x5chain).encode()

        # A little switcharoo to avoid cached overload_fields on `data`
        bib = CanonicalBlock(
            type_code=BlockIntegrityBlock._overload_fields[CanonicalBlock]
            ['type_code'],
            block_num=ctr.get_block_num(),
            crc_type=AbstractBlock.CrcType.CRC32,
        )
        bib_data = BlockIntegrityBlock(
            targets=target_block_nums,
            context_id=BPSEC_COSE_CONTEXT_ID,
            context_flags=(AbstractSecurityBlock.Flag.PARAMETERS_PRESENT),
            source=self._config.node_id,
            parameters=[
                TypeValuePair(type_code=5, value=aad_scope),
            ],
        )
        # Inject optional additional headers
        addl_protected = cbor2.dumps(
            addl_protected_map) if addl_protected_map else b''
        if addl_protected:
            bib_data.parameters.append(
                TypeValuePair(type_code=3, value=addl_protected))
        if addl_unprotected:
            bib_data.parameters.append(
                TypeValuePair(type_code=4, value=addl_unprotected))

        try:
            cose_key = self.extract_cose_key(self._priv_key)
        except Exception as err:
            LOGGER.error('Cannot handle private key: %s', repr(err))
            return

        phdr = {
            headers.Algorithm: cose_key.alg,
        }
        uhdr = {
            headers.X5t:
            X5T.from_certificate(algorithms.Sha256, x5chain[0]).encode(),
        }

        # Sign each target with one result per
        target_result = []
        for blk_num in bib_data.getfieldval('targets'):
            target_blk = ctr.block_num(blk_num)
            target_blk.ensure_block_type_specific_data()
            target_plaintext = target_blk.getfieldval('btsd')

            ext_aad_enc = CoseContext.get_bpsec_cose_aad(
                ctr, target_blk, bib, aad_scope, addl_protected)
            LOGGER.debug('Signing target %d AAD %s payload %s', blk_num,
                         encode_diagnostic(ext_aad_enc),
                         encode_diagnostic(target_plaintext))
            msg_obj = Sign1Message(
                phdr=phdr,
                uhdr=uhdr,
                payload=target_plaintext,
                # Non-encoded parameters
                external_aad=ext_aad_enc,
                key=cose_key)
            LOGGER.debug('Signing with COSE key %s', repr(cose_key))
            msg_enc = msg_obj.encode(tag=False)
            # detach payload
            msg_dec = cbor2.loads(msg_enc)
            msg_dec[2] = None
            msg_enc = cbor2.dumps(msg_dec)
            LOGGER.debug('Sending COSE message %s', encode_diagnostic(msg_dec))

            target_result.append(
                TypeValuePair(type_code=msg_obj.cbor_tag, value=msg_enc))

        # One result per target
        bib_data.setfieldval(
            'results',
            [TargetResultList(results=[result]) for result in target_result])
        bib.add_payload(bib_data)
        ctr.add_block(bib)
예제 #7
0
 def i2repr(self, pkt, x):
     return encode_diagnostic(x)
예제 #8
0
    def post_dissect(self, s):
        # Extract payload from fields
        pay_type = self.fields.get('type_code')
        pay_data = self.fields.get('btsd')
        if (pay_data is not None and pay_type is not None):
            try:
                cls = self.guess_payload_class(None)
                LOGGER.debug('CanonicalBlock.post_dissect with class %s from: %s', cls, encode_diagnostic(pay_data))
            except KeyError:
                cls = None

            if cls is not None:
                try:
                    pay = cls(pay_data)
                    self.add_payload(pay)
                except Exception as err:
                    if conf.debug_dissector:
                        raise
                    LOGGER.warning('CanonicalBlock failed to dissect payload: %s', err)

        return super().post_dissect(s)