Пример #1
0
    def unprotect(self, protected_message, request_id=None):
        assert (request_id is not None) == protected_message.code.is_response()
        is_response = protected_message.code.is_response()

        # Set to a raisable exception on replay check failures; it will be
        # raised, but the package may still be processed in the course of Echo handling.
        replay_error = None

        protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0(
            protected_message)

        if protected:
            raise ProtectionInvalid("The protected field is not empty")

        # FIXME check for duplicate keys in protected

        if unprotected.pop(COSE_KID, self.recipient_id) != self.recipient_id:
            # for most cases, this is caught by the session ID dispatch, but in
            # responses (where explicit sender IDs are atypical), this is a
            # valid check
            raise ProtectionInvalid("Sender ID does not match")

        if COSE_PIV not in unprotected:
            if not is_response:
                raise ProtectionInvalid(
                    "No sequence number provided in request")

            nonce = request_id.nonce
            seqno = None  # sentinel for not striking out anyting
        else:
            partial_iv_short = unprotected[COSE_PIV]

            nonce = self._construct_nonce(partial_iv_short, self.recipient_id)

            seqno = int.from_bytes(partial_iv_short, 'big')

            if not is_response:
                if not self.recipient_replay_window.is_initialized():
                    replay_error = ReplayError(
                        "Sequence number check unavailable")
                elif not self.recipient_replay_window.is_valid(seqno):
                    replay_error = ReplayError("Sequence number was re-used")

                if replay_error is not None and self.echo_recovery is None:
                    # Don't even try decoding if there is no reason to
                    raise replay_error

                request_id = RequestIdentifiers(self.recipient_id,
                                                partial_iv_short,
                                                nonce,
                                                can_reuse_nonce=self.is_unicast
                                                and replay_error is None)

        # FIXME is it an error for additional data to be present in unprotected?

        if len(
                ciphertext
        ) < self.algorithm.tag_bytes + 1:  # +1 assures access to plaintext[0] (the code)
            raise ProtectionInvalid("Ciphertext too short")

        enc_structure = [
            'Encrypt0', protected_serialized,
            self._extract_external_aad(protected_message, request_id.kid,
                                       request_id.partial_iv)
        ]
        aad = cbor.dumps(enc_structure)

        plaintext = self.algorithm.decrypt(ciphertext, aad, self.recipient_key,
                                           nonce)

        if not is_response and seqno is not None and replay_error is None:
            self.recipient_replay_window.strike_out(seqno)

        # FIXME add options from unprotected

        unprotected_message = Message(code=plaintext[0])
        unprotected_message.payload = unprotected_message.opt.decode(
            plaintext[1:])

        try_initialize = not self.recipient_replay_window.is_initialized() and \
                self.echo_recovery is not None
        if try_initialize:
            if protected_message.code.is_request():
                # Either accept into replay window and clear replay error, or raise
                # something that can turn into a 4.01,Echo response
                if unprotected_message.opt.echo == self.echo_recovery:
                    self.recipient_replay_window.initialize_from_freshlyseen(
                        seqno)
                    replay_error = None
                else:
                    raise ReplayErrorWithEcho(secctx=self,
                                              request_id=request_id,
                                              echo=self.echo_recovery)
            else:
                # We can initialize the replay window from a response as well.
                # The response is guaranteed fresh as it was AEAD-decoded to
                # match a request sent by this process.
                #
                # This is rare, as it only works when the server uses an own
                # sequence number, eg. when sending a notification or when
                # acting again on a retransmitted safe request whose response
                # it did not cache.
                #
                # Nothing bad happens if we can't make progress -- we just
                # don't initialize the replay window that wouldn't have been
                # checked for a response anyway.
                if seqno is not None:
                    self.recipient_replay_window.initialize_from_freshlyseen(
                        seqno)

        if replay_error is not None:
            raise replay_error

        if unprotected_message.code.is_request():
            if protected_message.opt.observe != 0:
                unprotected_message.opt.observe = None
        else:
            if protected_message.opt.observe is not None:
                # -1 ensures that they sort correctly in later reordering
                # detection. Note that neither -1 nor high (>3 byte) sequence
                # numbers can be serialized in the Observe option, but they are
                # in this implementation accepted for passing around.
                unprotected_message.opt.observe = -1 if seqno is None else seqno

        return unprotected_message, request_id
Пример #2
0
    def protect(self, message, request_partiv=None):
        # not trying to preserve token or mid, they're up to the transport
        outer_message = Message(code=message.code)
        if message.code.is_request():
            protected_uri = message.get_request_uri()
            if protected_uri.count('/') >= 3:
                protected_uri = protected_uri[:protected_uri.index(
                    '/',
                    protected_uri.index('/',
                                        protected_uri.index('/') + 1) + 1)]
            outer_message.set_request_uri(protected_uri)

        # FIXME any options to move out?
        inner_message = message

        if request_partiv is None:
            assert inner_message.code.is_request(
            ), "Trying to protect a response without request IV (possibly this is an observation; that's not supported in this OSCOAP implementation yet)"

            seqno = self.new_sequence_number()
            partial_iv = binascii.unhexlify(
                ("%%0%dx" % (2 * self.algorithm.iv_bytes)) % seqno)
            partial_iv_short = partial_iv.lstrip(b'\0')
            iv = _xor_bytes(self.sender_iv, partial_iv)

            unprotected = {
                6: partial_iv_short,
                4: self.sender_id,
            }
            request_kid = self.sender_id
        else:
            assert inner_message.code.is_response()

            partial_iv = request_partiv
            partial_iv_short = partial_iv.lstrip(b"\x00")
            iv = _flip_first_bit(_xor_bytes(partial_iv, self.sender_iv))
            unprotected = {}

            # FIXME: better should mirror what was used in request
            request_kid = self.recipient_id

        protected = {}

        assert protected == {}
        protected_serialized = b''  # were it into an empty dict, it'd be the cbor dump
        enc_structure = [
            'Encrypt0', protected_serialized,
            self._extract_external_aad(outer_message, request_kid,
                                       partial_iv_short)
        ]
        aad = cbor.dumps(enc_structure)
        key = self.sender_key

        plaintext = inner_message.opt.encode()
        if inner_message.payload:
            plaintext += bytes([0xFF])
            plaintext += inner_message.payload

        ciphertext, tag = self.algorithm.encrypt(plaintext, aad, key, iv)

        if USE_COMPRESSION:
            if protected:
                raise RuntimeError(
                    "Protection produced a message that has uncompressable fields."
                )
            if sorted(unprotected.keys()) == [4, 6]:
                shortarray = [unprotected[6], unprotected[4]]
                shortarray = cbor.dumps(shortarray)
                # we're using a shortarray shortened by one because that makes
                # it easier to then "exclude [...] the type and length for the
                # ciphertext"; the +1 on shortarray[0] makes it appear like a
                # 3-long array again.
                if (shortarray[0] + 1) & 0b11111000 != 0b10000000 or \
                        shortarray[1] & 0b11000000 != 0b01000000:
                    raise RuntimeError(
                        "Protection produced a message that has uncmpressable lengths"
                    )
                shortarray = bytes(
                    ((((shortarray[0] + 1) & 0b111) << 3) |
                     (shortarray[1] & 0b111), )) + shortarray[2:]
                oscoap_data = shortarray + ciphertext + tag
            elif unprotected == {}:
                oscoap_data = ciphertext + tag
            else:
                raise RuntimeError(
                    "Protection produced a message that has uncompressable fields."
                )
        else:
            cose_encrypt0 = [
                protected_serialized, unprotected, ciphertext + tag
            ]
            oscoap_data = cbor.dumps(cose_encrypt0)

        if inner_message.code.can_have_payload():
            outer_message.opt.object_security = b''
            outer_message.payload = oscoap_data
        else:
            outer_message.opt.object_security = oscoap_data

        # FIXME go through options section
        return outer_message, partial_iv
Пример #3
0
    def unprotect(self, protected_message, request_id=None):
        assert (request_id is not None) == protected_message.code.is_response()

        protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0(protected_message)

        if protected:
            raise ProtectionInvalid("The protected field is not empty")

        # FIXME check for duplicate keys in protected

        if unprotected.pop(COSE_KID, self.recipient_id) != self.recipient_id:
            # for most cases, this is caught by the session ID dispatch, but in
            # responses (where explicit sender IDs are atypical), this is a
            # valid check
            raise ProtectionInvalid("Sender ID does not match")

        if COSE_PIV not in unprotected:
            if request_id is None:
                raise ProtectionInvalid("No sequence number provided in request")

            nonce = request_id.nonce
            seqno = None # sentinel for not striking out anyting
        else:
            partial_iv_short = unprotected[COSE_PIV]

            seqno = int.from_bytes(partial_iv_short, 'big')

            if not self.recipient_replay_window.is_valid(seqno):
                # If here we ever implement something that accepts memory loss
                # as in 7.5.2 ("Losing Part of the Context State" / "Replay
                # window"), or an optimization that accepts replays to avoid
                # storing responses for EXCHANGE_LIFETIM, can_reuse_nonce a few
                # lines down needs to take that into consideration.
                raise ReplayError("Sequence number was re-used")

            nonce = self._construct_nonce(partial_iv_short, self.recipient_id)

            if request_id is None: # ie. we're unprotecting a request
                request_id = RequestIdentifiers(self.recipient_id, partial_iv_short, nonce, can_reuse_nonce=self.is_unicast)

        # FIXME is it an error for additional data to be present in unprotected?

        if len(ciphertext) < self.algorithm.tag_bytes + 1: # +1 assures access to plaintext[0] (the code)
            raise ProtectionInvalid("Ciphertext too short")

        enc_structure = ['Encrypt0', protected_serialized, self._extract_external_aad(protected_message, request_id.kid, request_id.partial_iv)]
        aad = cbor.dumps(enc_structure)

        plaintext = self.algorithm.decrypt(ciphertext, aad, self.recipient_key, nonce)

        if seqno is not None:
            self.recipient_replay_window.strike_out(seqno)

        # FIXME add options from unprotected

        unprotected_message = Message(code=plaintext[0])
        unprotected_message.payload = unprotected_message.opt.decode(plaintext[1:])

        if unprotected_message.code.is_request():
            if protected_message.opt.observe != 0:
                unprotected_message.opt.observe = None
        else:
            if protected_message.opt.observe is not None:
                # -1 ensures that they sort correctly in later reordering
                # detection. Note that neither -1 nor high (>3 byte) sequence
                # numbers can be serialized in the Observe option, but they are
                # in this implementation accepted for passing around.
                unprotected_message.opt.observe = -1 if seqno is None else seqno

        return unprotected_message, request_id
Пример #4
0
    def unprotect(self, protected_message, request_data=None):
        assert (request_data
                is not None) == protected_message.code.is_response()
        if request_data is not None:
            request_kid, request_partiv = request_data

        protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0(
            protected_message, is_request=request_data is None)

        if protected:
            raise ProtectionInvalid("The protected field is not empty")

        # FIXME check for duplicate keys in protected

        if unprotected.pop(4, self.recipient_id) != self.recipient_id:
            # for most cases, this is caught by the session ID dispatch, but in
            # responses (where explicit sender IDs are atypical), this is a
            # valid check
            raise ProtectionInvalid("Sender ID does not match")

        if 6 not in unprotected:
            if request_data is None:
                raise ProtectonInvalid(
                    "No sequence number provided in request")
            partial_iv = _pad_iv(self.algorithm, request_partiv)
            iv = _flip_first_bit(_xor_bytes(partial_iv, self.recipient_iv))
            seqno = None  # sentinel for not striking out anyting
        else:
            partial_iv_short = unprotected[6]

            if request_data is None:
                request_partiv = partial_iv_short
                request_kid = self.recipient_id

            seqno = int.from_bytes(partial_iv_short, 'big')

            if not self.recipient_replay_window.is_valid(seqno):
                raise ReplayError("Sequence number was re-used")

            partial_iv = _pad_iv(self.algorithm, partial_iv_short)
            iv = _xor_bytes(self.recipient_iv, partial_iv)

        # FIXME is it an error for additional data to be present in unprotected?

        if len(ciphertext) < self.algorithm.tag_bytes:
            raise ProtectionInvalid("Ciphertext shorter than tag length")

        tag = ciphertext[-self.algorithm.tag_bytes:]
        ciphertext = ciphertext[:-self.algorithm.tag_bytes]

        enc_structure = [
            'Encrypt0', protected_serialized,
            self._extract_external_aad(protected_message, request_kid,
                                       request_partiv)
        ]
        aad = cbor.dumps(enc_structure)

        plaintext = self.algorithm.decrypt(ciphertext, tag, aad,
                                           self.recipient_key, iv)

        if seqno is not None:
            self.recipient_replay_window.strike_out(seqno)

        # FIXME add options from unprotected

        unprotected_message = Message(code=protected_message.code)
        unprotected_message.payload = unprotected_message.opt.decode(plaintext)

        if unprotected_message.code.is_request:
            unprotected_message.opt.observe = protected_message.opt.observe
        else:
            if protected_message.opt.observe is not None:
                # is it really be as easy as that?
                unprotected_message.opt.observe = seqno

        return unprotected_message, (request_kid, request_partiv)
Пример #5
0
    def unprotect(self, protected_message, request_id=None):
        assert (request_id is not None) == protected_message.code.is_response()

        protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0(
            protected_message)

        if protected:
            raise ProtectionInvalid("The protected field is not empty")

        # FIXME check for duplicate keys in protected

        if unprotected.pop(COSE_KID, self.recipient_id) != self.recipient_id:
            # for most cases, this is caught by the session ID dispatch, but in
            # responses (where explicit sender IDs are atypical), this is a
            # valid check
            raise ProtectionInvalid("Sender ID does not match")

        if COSE_PIV not in unprotected:
            if request_id is None:
                raise ProtectionInvalid(
                    "No sequence number provided in request")

            nonce = request_id.nonce
            seqno = None  # sentinel for not striking out anyting
        else:
            partial_iv_short = unprotected[COSE_PIV]

            seqno = int.from_bytes(partial_iv_short, 'big')

            if not self.recipient_replay_window.is_valid(seqno):
                # If here we ever implement something that accepts memory loss
                # as in 7.5.2 ("Losing Part of the Context State" / "Replay
                # window"), or an optimization that accepts replays to avoid
                # storing responses for EXCHANGE_LIFETIM, can_reuse_nonce a few
                # lines down needs to take that into consideration.
                raise ReplayError("Sequence number was re-used")

            nonce = self._construct_nonce(partial_iv_short, self.recipient_id)

            if request_id is None:  # ie. we're unprotecting a request
                request_id = RequestIdentifiers(
                    self.recipient_id,
                    partial_iv_short,
                    nonce,
                    can_reuse_nonce=self.is_unicast)

        # FIXME is it an error for additional data to be present in unprotected?

        if len(
                ciphertext
        ) < self.algorithm.tag_bytes + 1:  # +1 assures access to plaintext[0] (the code)
            raise ProtectionInvalid("Ciphertext too short")

        enc_structure = [
            'Encrypt0', protected_serialized,
            self._extract_external_aad(protected_message, request_id.kid,
                                       request_id.partial_iv)
        ]
        aad = cbor.dumps(enc_structure)

        plaintext = self.algorithm.decrypt(ciphertext, aad, self.recipient_key,
                                           nonce)

        if seqno is not None:
            self.recipient_replay_window.strike_out(seqno)

        # FIXME add options from unprotected

        unprotected_message = Message(code=plaintext[0])
        unprotected_message.payload = unprotected_message.opt.decode(
            plaintext[1:])

        if unprotected_message.code.is_request():
            if protected_message.opt.observe != 0:
                unprotected_message.opt.observe = None
        else:
            if protected_message.opt.observe is not None:
                # -1 ensures that they sort correctly in later reordering
                # detection. Note that neither -1 nor high (>3 byte) sequence
                # numbers can be serialized in the Observe option, but they are
                # in this implementation accepted for passing around.
                unprotected_message.opt.observe = -1 if seqno is None else seqno

        return unprotected_message, request_id