Esempio n. 1
0
    def create_message_one(self) -> bytes:
        self._generate_ephemeral_key()

        self.msg_1 = MessageOne(
            method_corr=self._method * 4 + self._corr,
            cipher_suites=self.supported_ciphers,
            selected_cipher=self._selected_cipher,
            g_x=self.g_x,
            conn_idi=self._conn_id,
        )

        self._internal_state = EdhocState.MSG_1_SENT

        return self.msg_1.encode(self.corr)
Esempio n. 2
0
    def create_message_two(self, message_one: bytes) -> bytes:
        """
        Decodes an incoming EDHOC message 1 and creates and EDHOC message 2 or error message based on the content
        of message 1.

        :param message_one: Bytes representing an EDHOC message 1.
        :returns: Bytes of an EDHOC message 2 or an EDHOC error message.
        """

        self.msg_1 = MessageOne.decode(message_one)

        self._internal_state = EdhocState.MSG_1_RCVD

        if not self._verify_cipher_selection(self.msg_1.selected_cipher,
                                             self.msg_1.cipher_suites):
            self._internal_state = EdhocState.EDHOC_FAIL

            return MessageError(err_msg="").encode()

        if self.aad1_cb is not None:
            self.aad1_cb(self.msg_1.aad1)

        self._generate_ephemeral_key()

        self.msg_2 = MessageTwo(self.g_y, self.conn_idr, self.ciphertext_2,
                                self.conn_idi)

        self._internal_state = EdhocState.MSG_2_SENT
        return self.msg_2.encode(self.corr)
def test_initiator_finalize(initiator, test_vectors):
    initiator.msg_1 = MessageOne.decode(initiator.create_message_one())
    initiator.msg_2 = MessageTwo.decode(test_vectors['R']['message_2'])
    initiator.msg_3 = MessageThree.decode(
        initiator.create_message_three(test_vectors['R']['message_2']))

    c_i, c_r, app_aead, app_hash = initiator.finalize()

    assert c_i == test_vectors['I']['conn_id']
    assert c_r == test_vectors['R']['conn_id']
    assert app_aead == CipherSuite(test_vectors['I']['selected']).app_aead.id
    assert app_hash == CipherSuite(test_vectors['I']['selected']).app_hash.id
def test_initiator_finalize(initiator, test_vectors):
    initiator.msg_1 = MessageOne.decode(initiator.create_message_one())
    initiator.msg_2 = MessageTwo.decode(test_vectors['S']['message_2'])
    if getattr(initiator, 'remote_authkey', None) is None:
        warnings.warn(NoRemoteKey())
        return

    initiator.msg_3 = MessageThree.decode(initiator.create_message_three(test_vectors['S']['message_2']))

    c_i, c_r, app_aead, app_hash = initiator.finalize()

    assert c_i == test_vectors['I']['conn_id']
    assert c_r == test_vectors['R']['conn_id']
    assert app_aead == CipherSuite.from_id(test_vectors['I']['selected']).app_aead.identifier
    assert app_hash == CipherSuite.from_id(test_vectors['I']['selected']).app_hash.identifier
def test_initiator_message3(initiator, test_vectors):
    initiator.msg_1 = MessageOne.decode(test_vectors['I']['message_1'])
    initiator.msg_2 = MessageTwo.decode(test_vectors['R']['message_2'])

    crv = CoseEllipticCurves(CipherSuite(initiator._selected_cipher).dh_curve)
    hash_func = config_cose(CipherSuite(initiator._selected_cipher).hash).hash

    assert initiator.data_2 == test_vectors['R']['data_2']
    assert initiator._th2_input == test_vectors['R']['input_th_2']
    assert initiator._prk2e == test_vectors['R']['prk_2e']
    assert initiator._prk3e2m == test_vectors['R']['prk_3e2m']
    assert initiator.transcript(
        hash_func, initiator._th2_input) == test_vectors['R']['th_2']

    assert initiator._decrypt(
        initiator.msg_2.ciphertext) == test_vectors['R']['p_2e']

    assert initiator.shared_secret(initiator.ephemeral_key,
                                   OKP(x=initiator.g_y,
                                       crv=crv)) == test_vectors['S']['g_xy']
    assert initiator.data_3 == test_vectors['I']['data_3']
    assert initiator._th3_input == test_vectors['I']['input_th_3']
    assert initiator.transcript(
        hash_func, initiator._th3_input) == test_vectors['I']['th_3']
    assert initiator.cred_id == test_vectors['I']['id_cred']
    assert initiator._prk4x3m == test_vectors['I']['prk_4x3m']
    assert initiator._external_aad(
        initiator._th3_input,
        initiator.aad3_cb) == test_vectors['I']['eaad_3m']
    assert initiator._hkdf3(16, 'K_3m',
                            initiator._prk4x3m) == test_vectors['I']['k_3m']
    assert initiator._hkdf3(13, 'IV_3m',
                            initiator._prk4x3m) == test_vectors['I']['iv_3m']
    assert initiator._mac(initiator._hkdf3, 'K_3m', 16, 'IV_3m', 13,
                          initiator._th3_input, initiator._prk4x3m,
                          initiator.aad2_cb) == test_vectors['I']['mac3']
    assert initiator.signature_or_mac3(
        test_vectors['I']['mac3']) == test_vectors['I']['sign_or_mac3']
    assert initiator._p_3ae == test_vectors['I']['p_3ae']
    assert initiator._hkdf3(16, 'K_3ae',
                            initiator._prk3e2m) == test_vectors['I']['k_3ae']
    assert initiator._hkdf3(13, 'IV_3ae',
                            initiator._prk3e2m) == test_vectors['I']['iv_3ae']
    assert initiator.ciphertext_3 == test_vectors['I']['ciphertext_3']

    assert initiator.create_message_three(
        test_vectors['R']['message_2']) == test_vectors['I']['message_3']
def test_initiator_message3(initiator, test_vectors):
    initiator.msg_1 = MessageOne.decode(test_vectors['S']['message_1'])
    initiator.msg_2 = MessageTwo.decode(test_vectors['S']['message_2'])

    crv = CipherSuite.from_id(initiator._selected_cipher).dh_curve
    hash_func = CipherSuite.from_id(initiator._selected_cipher).hash.hash_cls

    assert initiator.data_2 == test_vectors['S']['data_2']
    assert initiator._th2_input == test_vectors['S']['input_th_2']
    assert initiator._prk2e == test_vectors['S']['prk_2e']
    assert initiator._prk3e2m == test_vectors['S']['prk_3e2m']
    assert initiator.transcript(hash_func, initiator._th2_input) == test_vectors['S']['th_2']

    assert initiator._decrypt(initiator.msg_2.ciphertext) == test_vectors['S']['p_2e']

    assert initiator.shared_secret(initiator.ephemeral_key, OKPKey(x=initiator.g_y, crv=crv)) == test_vectors['S'][
        'g_xy']
    assert initiator.data_3 == test_vectors['S']['data_3']
    assert initiator._th3_input == test_vectors['S']['input_th_3']
    assert initiator.transcript(hash_func, initiator._th3_input) == test_vectors['S']['th_3']
    assert initiator.cred_id == cbor2.loads(test_vectors['I']['cred_id'])
    assert initiator._prk4x3m == test_vectors['S']['prk_4x3m']
    assert initiator._hkdf3(16, 'K_3m', initiator._prk4x3m) == test_vectors['S']['k_3m']
    assert initiator._hkdf3(13, 'IV_3m', initiator._prk4x3m) == test_vectors['S']['iv_3m']
    assert initiator._mac(
        initiator.cred_idi,
        initiator.cred,
        initiator._hkdf3,
        'K_3m',
        16,
        'IV_3m',
        13,
        initiator._th3_input,
        initiator._prk4x3m,
        initiator.aad2_cb) == test_vectors['S']['mac_3']
    assert initiator.signature_or_mac3(test_vectors['S']['mac_3']) == test_vectors['S']['signature_3']
    assert initiator._p_3ae == test_vectors['S']['p_3ae']
    assert initiator._hkdf3(16, 'K_3ae', initiator._prk3e2m) == test_vectors['S']['k_3ae']
    assert initiator._hkdf3(13, 'IV_3ae', initiator._prk3e2m) == test_vectors['S']['iv_3ae']
    assert initiator.ciphertext_3 == test_vectors['S']['ciphertext_3']

    if initiator.remote_authkey is None:
        warnings.warn(NoRemoteKey())
        return
    assert initiator.create_message_three(test_vectors['S']['message_2']) == test_vectors['S']['message_3']
Esempio n. 7
0
class Initiator(EdhocRole):
    role = 'I'
    remote_role = 'R'

    def __init__(self,
                 corr: Correlation,
                 method: Method,
                 cred: Union[RPK, Certificate],
                 cred_idi: CoseHeaderMap,
                 auth_key: RPK,
                 selected_cipher: Type['CS'],
                 supported_ciphers: List[Type['CS']],
                 remote_cred_cb: Callable[[CoseHeaderMap], Union[Certificate,
                                                                 RPK]],
                 conn_idi: Optional[bytes] = None,
                 aad1_cb: Optional[Callable[..., bytes]] = None,
                 aad2_cb: Optional[Callable[..., bytes]] = None,
                 aad3_cb: Optional[Callable[..., bytes]] = None,
                 ephemeral_key: Optional['CK'] = None):
        """
        Create an EDHOC Initiator.

        :param corr: Correlation value (depends on the transport protocol).
        :param method: EDHOC method type (signatures, static DH or a mix).
        :param cred: The public authentication credentials of the Initiator.
        :param cred_idi: The Initiator's credential identifier (a CBOR encoded COSE header map)
        :param auth_key: The private authentication key (CoseKey) of the Responder.
        :param selected_cipher: Provide the selected cipher.
        :param supported_ciphers: A list of ciphers supported by the Responder.
        :param conn_idi: The connection identifier to be used
        :param remote_cred_cb: A callback that fetches the remote credentials.
        :param aad1_cb: A callback to pass received additional data to the application protocol.
        :param aad2_cb: A callback to pass additional data to the remote endpoint.
        :param aad3_cb: A callback to pass received additional data to the application protocol.
        :param ephemeral_key: Preload an (CoseKey) ephemeral key (if unset a random key will be generated).
        """

        if conn_idi is None:
            conn_idi = os.urandom(1)

        super().__init__(cred, cred_idi, auth_key, supported_ciphers, conn_idi,
                         remote_cred_cb, aad1_cb, aad2_cb, aad3_cb,
                         ephemeral_key)

        self._selected_cipher = CipherSuite.from_id(selected_cipher)
        self._corr = Correlation(corr)
        self._method = Method(method)

    @property
    def cipher_suite(self) -> 'CS':
        return self._selected_cipher

    @property
    def corr(self) -> Correlation:
        return self._corr

    @property
    def method(self) -> Method:
        return self._method

    @property
    def conn_idi(self):
        if self.corr in [Correlation.CORR_1, Correlation.CORR_3]:
            conn_idi = b''
        else:
            conn_idi = self._conn_id

        return conn_idi

    @property
    def conn_idr(self):
        if self.corr in [Correlation.CORR_2, Correlation.CORR_3]:
            conn_idr = b''
        else:
            conn_idr = self.msg_2.conn_idr

        return conn_idr

    @property
    def cred_idi(self) -> CoseHeaderMap:
        return self.cred_id

    @property
    def cred_idr(self) -> CoseHeaderMap:
        return self._cred_idr

    @cred_idr.setter
    def cred_idr(self, value):
        if isinstance(value, int):
            value = {4: EdhocMessage.decode_bstr_id(value)}
        elif isinstance(value, bytes):
            value = {4: value}

        self._cred_idr = value
        self._populate_remote_details(value)

    @property
    def g_y(self) -> bytes:
        return self.msg_2.g_y

    @property
    def g_x(self) -> bytes:

        self._generate_ephemeral_key()

        return self.ephemeral_key.x

    @property
    def local_pubkey(self) -> RPK:
        """ Returns the local ephemeral public key. """

        if self.cipher_suite.dh_curve in [X448, X25519]:
            return OKPKey(x=self.g_x, crv=self.cipher_suite.dh_curve)
        else:
            return EC2Key(x=self.g_x, crv=self.cipher_suite.dh_curve)

    @property
    def remote_pubkey(self) -> RPK:
        """ Returns the remote ephemeral public key. """

        if self.cipher_suite.dh_curve in [X448, X25519]:
            return OKPKey(x=self.g_y, crv=self.cipher_suite.dh_curve)
        else:
            return EC2Key(x=self.g_y, crv=self.cipher_suite.dh_curve)

    @property
    def local_authkey(self) -> RPK:
        return self._local_authkey

    def signature_or_mac3(self, mac_3: bytes):
        return self._signature_or_mac(mac_3, self._th3_input, self.aad3_cb)

    def create_message_one(self) -> bytes:
        self._generate_ephemeral_key()

        self.msg_1 = MessageOne(
            method_corr=self._method * 4 + self._corr,
            cipher_suites=self.supported_ciphers,
            selected_cipher=self._selected_cipher,
            g_x=self.g_x,
            conn_idi=self._conn_id,
        )

        self._internal_state = EdhocState.MSG_1_SENT

        return self.msg_1.encode(self.corr)

    def create_message_three(self, message_two: bytes):

        self.msg_2 = MessageTwo.decode(message_two)

        self._internal_state = EdhocState.MSG_2_RCVD

        decoded = EdhocMessage.decode(self._decrypt(self.msg_2.ciphertext))

        self.cred_idr = decoded[0]

        if not self._verify_signature_or_mac2(signature_or_mac2=decoded[1]):
            self._internal_state = EdhocState.EDHOC_FAIL
            return MessageError(
                err_msg='Signature verification failed').encode()

        try:
            ad_2 = decoded[2]
            if self.aad2_cb is not None:
                self.aad2_cb(ad_2)
        except IndexError:
            pass

        self.msg_3 = MessageThree(self.ciphertext_3, self.conn_idr)

        self._internal_state = EdhocState.MSG_3_SENT

        return self.msg_3.encode(self.corr)

    def _verify_signature_or_mac2(self, signature_or_mac2: bytes) -> bool:
        mac_2 = self._mac(self.cred_idr, self.remote_cred, self._hkdf2, 'K_2m',
                          16, 'IV_2m', 13, self._th2_input, self._prk3e2m,
                          self.aad2_cb)

        if not self.is_static_dh(self.remote_role):
            external_aad = self._external_aad(self.remote_cred,
                                              self._th2_input, self.aad2_cb)
            cose_sign = Sign1Message(
                phdr=self.cred_idr,
                uhdr={headers.Algorithm: self.cipher_suite.sign_alg},
                payload=mac_2,
                external_aad=external_aad)
            # FIXME peeking into internals (probably best resolved at pycose level)
            cose_sign.key = self.remote_authkey
            cose_sign._signature = signature_or_mac2
            return cose_sign.verify_signature()
        else:
            return signature_or_mac2 == mac_2

    def finalize(self) -> Tuple[bytes, bytes, int, int]:
        """
        Finalizes the key exchange.

        :return: A 4-tuple containing the initiator and responder's connection identifiers and the application AEAD and\
         hash algorithms.
        """

        self._internal_state = EdhocState.EDHOC_SUCC

        app_aead = self.cipher_suite.app_aead
        app_hash = self.cipher_suite.app_hash

        # pass the connection identifiers and the algorithms identifiers
        return self._conn_id, self.msg_2.conn_idr, app_aead.identifier, app_hash.identifier

    @property
    def ciphertext_3(self):
        # TODO: resolve magic key and IV lengths
        iv_bytes = self._hkdf3(13, 'IV_3ae', self._prk3e2m)

        # TODO: resolve magic key and IV lengths
        cose_key = self._create_cose_key(self._hkdf3, 16, 'K_3ae',
                                         self._prk3e2m, [EncryptOp])

        # create payload for the COSE_Encrypt0
        payload = [self._p_3ae]

        if self.aad3_cb is not None:
            payload.append(self.aad3_cb())

        payload = b''.join(payload)

        # create the external data for the COSE_Encrypt0
        th_3 = self.transcript(self.cipher_suite.hash.hash_cls,
                               self._th3_input)

        # calculate the mac_2 using a COSE_Encrypt0 message
        ciphertext = Enc0Message(uhdr={
            headers.IV: iv_bytes,
            headers.Algorithm: self.cipher_suite.aead
        },
                                 key=cose_key,
                                 payload=payload,
                                 external_aad=th_3).encrypt()

        return ciphertext

    @property
    def _hkdf2(self) -> Callable:
        return functools.partial(super()._hkdf_expand,
                                 transcript=self._th2_input)

    @property
    def _hkdf3(self) -> Callable:
        return functools.partial(super()._hkdf_expand,
                                 transcript=self._th3_input)

    def _prk3e2m_static_dh(self, prk: bytes):
        return self._prk(self.ephemeral_key, self.remote_authkey, prk)

    def _prk4x3m_static_dh(self, prk: bytes):
        return self._prk(self.auth_key, self.remote_pubkey, prk)

    @property
    def _p_3ae(self):
        # TODO: resolve magic key and IV lengths
        mac_3 = self._mac(self.cred_idi, self.cred, self._hkdf3, 'K_3m', 16,
                          'IV_3m', 13, self._th3_input, self._prk4x3m,
                          self.aad3_cb)

        signature = self.signature_or_mac3(mac_3)

        if KID.identifier in self.cred_id:
            cred_id = EdhocMessage.encode_bstr_id(self.cred_id[KID.identifier])
        else:
            cred_id = self.cred_id

        return b"".join([
            cbor2.dumps(cred_id, default=EdhocRole._custom_cbor_encoder),
            cbor2.dumps(signature)
        ])

    def _decrypt(self, ciphertext: bytes) -> bytes:
        length = len(ciphertext)
        xord = int.from_bytes(ciphertext, "big") ^ int.from_bytes(
            self._hkdf2(length, "KEYSTREAM_2", self._prk2e), "big")
        return xord.to_bytes((xord.bit_length() + 7) // 8, byteorder="big")
Esempio n. 8
0
class Initiator(EdhocRole):
    def __init__(self,
                 corr: Correlation,
                 method: Method,
                 cred: bytes,
                 cred_idi: CoseHeaderMap,
                 auth_key: Key,
                 selected_cipher: CipherSuite,
                 supported_ciphers: List[CipherSuite],
                 peer_cred: Optional[Union[Callable[..., bytes], bytes]],
                 conn_idi: Optional[bytes] = None,
                 aad1_cb: Optional[Callable[..., bytes]] = None,
                 aad2_cb: Optional[Callable[..., bytes]] = None,
                 aad3_cb: Optional[Callable[..., bytes]] = None,
                 ephemeral_key: Optional[Key] = None):
        """
        Create an EDHOC Initiator.

        :param corr: Correlation value (depends on the transport protocol).
        :param method: EDHOC method type (signatures, static DH or a mix).
        :param cred: The public authentication credentials of the Initiator.
        :param cred_idi: The Initiator's credential identifier (a CBOR encoded COSE header map)
        :param auth_key: The private authentication key (CoseKey) of the Responder.
        :param selected_cipher: Provide the selected cipher.
        :param supported_ciphers: A list of ciphers supported by the Responder.
        :param conn_idi: The connection identifier to be used
        :param peer_cred: Provide the public authentication material for the remote peer, by a callback or directly.
        :param aad1_cb: A callback to pass received additional data to the application protocol.
        :param aad2_cb: A callback to pass additional data to the remote endpoint.
        :param aad3_cb: A callback to pass received additional data to the application protocol.
        :param ephemeral_key: Preload an (CoseKey) ephemeral key (if unset a random key will be generated).
        """

        if conn_idi is None:
            conn_idi = os.urandom(1)

        super().__init__(cred, cred_idi, auth_key, supported_ciphers, conn_idi,
                         peer_cred, aad1_cb, aad2_cb, aad3_cb, ephemeral_key)

        self._selected_cipher = CipherSuite(selected_cipher)
        self._corr = Correlation(corr)
        self._method = Method(method)

        self._cred_idr = None

    @property
    def cipher_suite(self) -> CipherSuite:
        return self._selected_cipher

    @property
    def corr(self) -> Correlation:
        return self._corr

    @property
    def method(self) -> Method:
        return self._method

    @property
    def conn_idi(self):
        if self.corr in [Correlation.CORR_1, Correlation.CORR_3]:
            conn_idi = b''
        else:
            conn_idi = self._conn_id

        return conn_idi

    @property
    def conn_idr(self):
        if self.corr in [Correlation.CORR_2, Correlation.CORR_3]:
            conn_idr = b''
        else:
            conn_idr = self.msg_2.conn_idr

        return conn_idr

    @property
    def cred_idi(self) -> CoseHeaderMap:
        return self.cred_id

    @property
    def cred_idr(self) -> CoseHeaderMap:
        return self._cred_idr

    @property
    def g_y(self) -> bytes:
        return self.msg_2.g_y

    @property
    def g_x(self) -> bytes:

        self._generate_ephemeral_key()

        return self.ephemeral_key.x

    @property
    def local_pubkey(self) -> Key:
        """ Returns the local ephemeral public key. """

        if self.cipher_suite.dh_curve in [
                CoseEllipticCurves.X448, CoseEllipticCurves.X25519
        ]:
            return OKP(x=self.g_x, crv=self.cipher_suite.dh_curve)
        else:
            # TODO:
            pass

    @property
    def remote_pubkey(self) -> Key:
        """ Returns the remote ephemeral public key. """

        if self.cipher_suite.dh_curve in [
                CoseEllipticCurves.X448, CoseEllipticCurves.X25519
        ]:
            return OKP(x=self.g_y, crv=self.cipher_suite.dh_curve)
        else:
            # TODO:
            pass

    @property
    def local_authkey(self) -> Key:
        return self._local_authkey

    @property
    def remote_authkey(self) -> Key:
        if hasattr(self._remote_authkey, '__call__'):
            return self._remote_authkey(self.cred_idr)
        else:
            return self._remote_authkey

    @property
    def peer_cred(self):
        if hasattr(self._peer_cred, '__call__'):
            self._peer_cred(self.cred_idr)
        else:
            return self._peer_cred

    def signature_or_mac3(self, mac_3: bytes):
        return self._signature_or_mac(mac_3, self._th3_input, self.aad3_cb)

    def create_message_one(self) -> bytes:
        self._generate_ephemeral_key()

        self.msg_1 = MessageOne(
            method_corr=self._method * 4 + self._corr,
            cipher_suites=self.supported_ciphers,
            selected_cipher=self._selected_cipher,
            g_x=self.g_x,
            conn_idi=self._conn_id,
        )

        self._internal_state = EdhocState.MSG_1_SENT

        return self.msg_1.encode()

    def create_message_three(self, message_two: bytes):

        self.msg_2 = MessageTwo.decode(message_two)

        self._internal_state = EdhocState.MSG_2_RCVD

        decoded = EdhocMessage.decode(self._decrypt(self.msg_2.ciphertext))

        self._cred_idr = decoded[0]

        if not self._verify_signature(signature=decoded[1]):
            self._internal_state = EdhocState.EDHOC_FAIL
            return MessageError(err_msg='').encode()

        try:
            ad_2 = decoded[2]
            if self.aad2_cb is not None:
                self.aad2_cb(ad_2)
        except IndexError:
            pass

        self.msg_3 = MessageThree(self.ciphertext_3, self.conn_idr)

        self._internal_state = EdhocState.MSG_3_SENT

        return self.msg_3.encode()

    def finalize(self) -> Tuple[bytes, bytes, int, int]:
        """
        Finalizes the key exchange.

        :return: A 4-tuple containing the initiator and responder's connection identifiers and the application AEAD and\
         hash algorithms.
        """

        self._internal_state = EdhocState.EDHOC_SUCC

        app_aead = self.cipher_suite.app_aead
        app_hash = self.cipher_suite.app_hash

        # pass the connection identifiers and the algorithms identifiers
        return self._conn_id, self.msg_2.conn_idr, app_aead.id, app_hash.id

    @property
    def ciphertext_3(self):
        # TODO: resolve magic key and IV lengths
        iv_bytes = self._hkdf3(13, 'IV_3ae', self._prk3e2m)

        hash_func = config_cose(self.cipher_suite.hash).hash
        # TODO: resolve magic key and IV lengths
        cose_key = self._create_cose_key(self._hkdf3, 16, 'K_3ae',
                                         self._prk3e2m, KeyOps.ENCRYPT)

        # create payload for the COSE_Encrypt0
        payload = [self._p_3ae]

        if self.aad3_cb is not None:
            payload.append(self.aad3_cb())

        payload = b''.join(payload)

        # create the external data for the COSE_Encrypt0
        th_3 = self.transcript(hash_func, self._th3_input)

        # calculate the mac_2 using a COSE_Encrypt0 message
        ciphertext = Enc0Message(payload=payload, external_aad=th_3).encrypt(
            iv_bytes, cose_key)

        return ciphertext

    @property
    def _hkdf2(self) -> Callable:
        return functools.partial(super()._hkdf_expand,
                                 transcript=self._th2_input)

    @property
    def _hkdf3(self) -> Callable:
        return functools.partial(super()._hkdf_expand,
                                 transcript=self._th3_input)

    def _prk3e2m_static_dh(self, prk: bytes):
        return self._prk(self.ephemeral_key, self.remote_authkey, prk)

    def _prk4x3m_static_dh(self, prk: bytes):
        return self._prk(self.auth_key, self.remote_pubkey, prk)

    @property
    def _p_3ae(self):
        # TODO: resolve magic key and IV lengths
        mac_3 = self._mac(self._hkdf3, 'K_3m', 16, 'IV_3m', 13,
                          self._th3_input, self._prk4x3m, self.aad3_cb)

        signature = self.signature_or_mac3(mac_3)

        if CoseHeaderKeys.KID in self.cred_id:
            cred_id = EdhocMessage.encode_bstr_id(
                self.cred_id[CoseHeaderKeys.KID])
        else:
            cred_id = self.cred_id

        return b"".join([cbor2.dumps(cred_id), cbor2.dumps(signature)])

    def _decrypt(self, ciphertext: bytes) -> bytes:
        length = len(ciphertext)
        xord = int.from_bytes(ciphertext, "big") ^ int.from_bytes(
            self._hkdf2(length, "K_2e", self._prk2e), "big")
        return xord.to_bytes((xord.bit_length() + 7) // 8, byteorder="big")