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_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']
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")
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")