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
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)
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)
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
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
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)
def i2repr(self, pkt, x): return encode_diagnostic(x)
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)