Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to EDHOC -12 #30

Draft
wants to merge 46 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
dd90bd7
Validation error: produce error message
chrysn May 18, 2021
2d902d6
Update to EDHOC -06
chrysn May 18, 2021
46edbc6
Adjust to pycose v0.9dev7
chrysn May 18, 2021
644c5b6
Add support for P384
chrysn May 19, 2021
1442135
message: Decode from BytesIO to facilitate use with -12
chrysn Mar 19, 2022
6c661e9
create_message_two: Take parsed message one
chrysn Mar 19, 2022
e010332
msg1: None is not part of the message any more
chrysn Mar 19, 2022
cdaa14e
message 2: Remove ID_I (this is now sent out-of-band all the way)
chrysn Mar 19, 2022
9d4fb88
message 3: ID_R is not part of the message any more
chrysn Mar 19, 2022
88b41b2
bstr-id: Is not used any more in EDHOC itself
chrysn Mar 19, 2022
03d78f0
msg_2: Adjust to format change where g_x and ciphertext are concatenated
chrysn Mar 19, 2022
ecb7fef
messages: Refactor since structure decoding is more straightforward now
chrysn Mar 19, 2022
d0fd6ae
Correlation removed
chrysn Mar 19, 2022
6649b46
finalize: Take parsed message three
chrysn Mar 19, 2022
304837e
Deduplicate methods
chrysn Mar 20, 2022
f9cbf45
Interop state
chrysn Mar 22, 2022
14284dc
Update cipher suite definitions
chrysn Mar 22, 2022
ab62d0f
Sha384 is a SHA-2
chrysn Mar 22, 2022
27b2425
Restore signature verification
chrysn Mar 23, 2022
f243a0a
Remove dead code
chrysn Mar 23, 2022
fd58367
AAD handling
chrysn Mar 23, 2022
aabf4d3
Identifier modernization: conn_idx -> c_x
chrysn Mar 23, 2022
07adfb4
C_x can be bytes or int; consequently, must be given
chrysn Mar 23, 2022
6a2752e
Drop more dead code
chrysn Mar 23, 2022
32a5219
naming: Use id_cred_{r,i,local} rather than cred_id{r,i,}, ...
chrysn Mar 23, 2022
b15c064
Calculate mac_3 centrally
chrysn Mar 24, 2022
26d3905
Messge: Revert decode interface to bytes
chrysn Mar 26, 2022
0dd4de2
msg_1: Store encoded version for later hashing
chrysn Mar 26, 2022
b3ceeab
Simplify th_2 calculation
chrysn Mar 26, 2022
fdf4ba7
Use pycose provided hashing
chrysn Mar 26, 2022
06f070c
Remove leftover code
chrysn Mar 26, 2022
5604b02
Cache properties
chrysn Mar 26, 2022
48f7f9b
Remove unused code path
chrysn Mar 26, 2022
9a443a7
Do not store msg_1, store relevant individual attributes instead
chrysn Mar 26, 2022
58702a7
Store c_i and c_r in attributes
chrysn Mar 26, 2022
f2af922
Do not store msg_2, store c_r and g_y instead
chrysn Mar 26, 2022
fa8f937
Do not store msg_3
chrysn Mar 26, 2022
ce6007c
typing: Introduced Cred to prepare inclusion of CCS
chrysn Mar 26, 2022
7009a79
Prepare use of CWTs and CCSs rather than RPKs as creds (typing only)
chrysn Mar 26, 2022
5068cbb
Prepare use of CWTs and CCSs rather than RPKs as creds (typing only)
chrysn Mar 26, 2022
e56162e
Document in types what the tuples sometimes used for Cred mean
chrysn Mar 26, 2022
807ff79
Define and allow using CCSs
chrysn Mar 26, 2022
bc3f8af
Credential typing: CWTs are valid but can't be parsed into a key
chrysn Mar 26, 2022
f1c8c47
Credentials: Stop accepting RPKs
chrysn Mar 26, 2022
f28e8a6
Credentials: Improve type clarity
chrysn Mar 26, 2022
dc19867
cred_[ir], preencoded credentials: Take and produce serialized form
chrysn Mar 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 69 additions & 14 deletions edhoc/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@

import cbor2
from cose.algorithms import AESCCM1664128, Sha256, EdDSA, AESCCM16128128, Es256, A128GCM, A256GCM, Sha384, Es384
from cose.curves import X25519, Ed25519, P256, P384
from cose.keys.curves import X25519, Ed25519, P256, P384
from cose.keys import CoseKey

from edhoc.exceptions import EdhocException


def cborstream(items) -> bytes:
"""Encode an iterable of items in CBOR into a stream"""
return b"".join(cbor2.dumps(i) for i in items)

def compress_id_cred_x(id_cred_x):
if list(id_cred_x.keys()) == 4 and type(id_cred_x[4]) in (int, bytes):
return id_cred_x[4]
else:
return id_cred_x

def bytewise_xor(a: bytes, b: bytes) -> bytes:
assert len(a) == len(b) # Python 3.10: zip(a, b, True) and remove this line
return bytes((_a ^ _b) for (_a, _b) in zip(a, b))

class EdhocState(IntEnum):
EDHOC_WAIT = 0
MSG_1_SENT = 1
Expand Down Expand Up @@ -117,9 +132,9 @@ def check_identifiers(cls):
return (
cls.aead.identifier,
cls.hash.identifier,
cls.edhoc_mac_length,
cls.dh_curve.identifier,
cls.sign_alg.identifier,
cls.sign_curve.identifier,
cls.app_aead.identifier,
cls.app_hash.identifier,
)
Expand All @@ -138,6 +153,9 @@ class CipherSuite0(CipherSuite):
app_aead = AESCCM1664128
app_hash = Sha256

edhoc_mac_length = 8
assert CipherSuite0.check_identifiers() == (10, -16, 8, 4, -8, 10, -16)


@CipherSuite.register_ciphersuite()
class CipherSuite1(CipherSuite):
Expand All @@ -152,6 +170,9 @@ class CipherSuite1(CipherSuite):
app_aead = AESCCM1664128
app_hash = Sha256

edhoc_mac_length = 16
assert CipherSuite1.check_identifiers() == (30, -16, 16, 4, -8, 10, -16)


@CipherSuite.register_ciphersuite()
class CipherSuite2(CipherSuite):
Expand All @@ -166,6 +187,9 @@ class CipherSuite2(CipherSuite):
app_aead = AESCCM1664128
app_hash = Sha256

edhoc_mac_length = 8
assert CipherSuite2.check_identifiers() == (10, -16, 8, 1, -7, 10, -16)


@CipherSuite.register_ciphersuite()
class CipherSuite3(CipherSuite):
Expand All @@ -180,10 +204,15 @@ class CipherSuite3(CipherSuite):
app_aead = AESCCM1664128
app_hash = Sha256

edhoc_mac_length = 16
assert CipherSuite3.check_identifiers() == (30, -16, 16, 1, -7, 10, -16)

# ChaCha missing from pycose

@CipherSuite.register_ciphersuite()
class CipherSuite4(CipherSuite):
identifier = 4
fullname = "SUITE_4"
class CipherSuite6(CipherSuite):
identifier = 6
fullname = "SUITE_6"

aead = A128GCM
hash = Sha256
Expand All @@ -192,12 +221,14 @@ class CipherSuite4(CipherSuite):
sign_curve = P256
app_aead = A128GCM
app_hash = Sha256
assert CipherSuite4.check_identifiers() == (1, -16, 4, -7, 1, 1, -16)

edhoc_mac_length = 16
assert CipherSuite6.check_identifiers() == (1, -16, 16, 4, -7, 1, -16)

@CipherSuite.register_ciphersuite()
class CipherSuite5(CipherSuite):
identifier = 5
fullname = "SUITE_5"
class CipherSuite24(CipherSuite):
identifier = 24
fullname = "SUITE_24"

aead = A256GCM
hash = Sha384
Expand All @@ -206,20 +237,44 @@ class CipherSuite5(CipherSuite):
sign_curve = P384
app_aead = A256GCM
app_hash = Sha384
assert CipherSuite5.check_identifiers() == (3, -43, 2, -35, 2, 3, -43)

edhoc_mac_length = 16
assert CipherSuite24.check_identifiers() == (3, -43, 16, 2, -35, 3, -43)


class EdhocKDFInfo(NamedTuple):
edhoc_aead_id: int
transcript_hash: bytes
label: str
context: bytes
length: int

def encode(self) -> bytes:
info = [self.edhoc_aead_id, self.transcript_hash, self.label, self.length]
info = cbor2.dumps(info)
return info
return cborstream(self)

class CCS:
"""A CWT Claims Set (containing an unencrypted COSE key in its CNF)"""
def __init__(self, encoded: bytes):
"""Decode a CWT. Raises some ValueError if the set can not be
decoded."""
self.encoded = encoded
self._set_details_from(cbor2.loads(encoded))

@classmethod
def from_unencoded(cls, unencoded: dict):
"""Load a CWT from a dictionary that all parties agree to encoded
canonically"""
# The optimization of going directly to _set_details is probably
# unwarranted.
return cls(cbor2.dumps(unencoded))

def _set_details_from(self, d: dict):
CWT_SUB = 2
CWT_CNF = 8
CNF_KEY = 1
self.sub = d.get(CWT_SUB, None)
cnf = d[CWT_CNF]
key = cnf[CNF_KEY]
self.key = CoseKey.from_dict(key)

CS = TypeVar('CS', bound='CipherSuite')

Expand Down
23 changes: 8 additions & 15 deletions edhoc/messages/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from abc import ABCMeta, abstractmethod
from typing import Union
from edhoc.definitions import Correlation
import warnings
from io import BytesIO

import cbor2

Expand All @@ -17,30 +19,21 @@ def decode(cls, received: bytes) -> list:
:return: a decode EDHOC message
"""

received = BytesIO(received)

decoded = []

while len(received) > 0:
decoded += [cbor2.loads(received)]
received = received[received.startswith(cbor2.dumps(decoded[-1])) and len(cbor2.dumps(decoded[-1])):]
total_length = len(received.getvalue())
while received.tell() < total_length:
decoded.append(cbor2.load(received))
return decoded

@abstractmethod
def encode(self, corr: Correlation):
def encode(self):
""" Encodes an EDHOC message as bytes, ready to be sent over reliable transport. """

raise NotImplementedError

@staticmethod
def encode_bstr_id(conn_id: bytes) -> Union[int, bytes]:
if len(conn_id) == 1:
return int.from_bytes(conn_id, byteorder='big') - 24
else:
return conn_id

@staticmethod
def decode_bstr_id(conn_id: int) -> bytes:
return int(conn_id + 24).to_bytes(1, byteorder="big")

@classmethod
def _truncate(cls, payload: bytes):
return f'{payload[:5]} ... ({len(payload)} bytes)'
73 changes: 31 additions & 42 deletions edhoc/messages/message1.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@


class MessageOne(EdhocMessage):
METHOD_CORR = 0
CIPHERS = 1
G_X = 2
CONN_ID = 3
AAD1 = 4
## Stored at decoding (or encoding, for sent messages) time to contain the
## full byte string of the message, including any padding. (Unlike later
## transcripts, the full message goes in).
encoded: bytes

@classmethod
def decode(cls, received: bytes) -> 'MessageOne':
def decode(cls, received) -> 'MessageOne':
"""
Tries to decode the bytes as an EDHOC MessageOne.

Expand All @@ -31,71 +30,58 @@ def decode(cls, received: bytes) -> 'MessageOne':

decoded = super().decode(received)

method_corr = decoded[cls.METHOD_CORR]
(method, ciphers, g_x, c_i, *aad) = decoded

if isinstance(decoded[cls.CIPHERS], int):
selected_cipher = decoded[cls.CIPHERS]
supported_ciphers = [decoded[cls.CIPHERS]]
if isinstance(ciphers, int):
selected_cipher = ciphers
supported_ciphers = [ciphers]
elif isinstance(decoded[cls.CIPHERS], list):
selected_cipher = decoded[cls.CIPHERS][0]
supported_ciphers = decoded[cls.CIPHERS][1:]
selected_cipher = ciphers[0]
supported_ciphers = ciphers[1:]
else:
raise EdhocInvalidMessage("Failed to decode bytes as MessageOne")

g_x = decoded[cls.G_X]

if decoded[cls.CONN_ID] != b'':
if isinstance(decoded[cls.CONN_ID], int):
conn_idi = EdhocMessage.decode_bstr_id(decoded[cls.CONN_ID])
else:
conn_idi = decoded[cls.CONN_ID]
else:
conn_idi = b''

msg = cls(
method_corr=method_corr,
method=method,
selected_cipher=CipherSuite.from_id(selected_cipher),
cipher_suites=[CipherSuite.from_id(c) for c in supported_ciphers],
g_x=g_x,
conn_idi=conn_idi)
c_i=c_i)

try:
msg.aad1 = decoded[cls.AAD1]
except IndexError:
pass
msg.encoded = received

if aad:
raise NotImplementedError("AAD changed")

return msg

def __init__(self,
method_corr: int,
method: int,
cipher_suites: List['CS'],
selected_cipher: Type['CS'],
g_x: bytes,
conn_idi: Optional[bytes] = None,
c_i: Optional[bytes] = None,
external_aad: bytes = b''):

"""
Creates an EDHOC MessageOne object.

:param method_corr: Combination of the method parameter and correlation parameter (4 * method + correlation)
:param method: EDHOC method (indicating who signs / who does static derivation)
:param cipher_suites: Supported cipher suites (ordered by decreasing preference).
:param selected_cipher: The preferred cipher suite.
:param g_x: The ephemeral public key of the Initiator.
:param conn_idi: A variable length connection identifier.
:param c_i: A variable length connection identifier.
:param external_aad: Unprotected opaque auxiliary data (transferred together with EDHOC message 1).
"""

self.method_corr = method_corr
self.method = method
self.cipher_suites = cipher_suites
self.selected_cipher = selected_cipher
self.g_x = g_x
self.conn_idi = conn_idi
self.c_i = c_i
self.aad1 = external_aad

self.corr = self.method_corr % 4
self.method = (self.method_corr - self.corr) // 4

def encode(self, corr: Correlation) -> bytes:
def encode(self) -> bytes:
"""
Encodes the first EDHOC message as a CBOR sequence.

Expand All @@ -109,16 +95,19 @@ def encode(self, corr: Correlation) -> bytes:
else:
raise ValueError('Cipher suite list must contain at least 1 item.')

msg = [self.method_corr, suites, self.g_x, self.encode_bstr_id(self.conn_idi)]
msg = [self.method, suites, self.g_x, self.c_i]

if self.aad1 != b'':
raise NotImplementedError("AAD stuff changed")
msg.append(cbor2.dumps(self.aad1))

return b"".join(cbor2.dumps(chunk) for chunk in msg)
# FIXME We might want to have "encode precisely once" semantics more generally
self.encoded = b"".join(cbor2.dumps(chunk) for chunk in msg)
return self.encoded

def __repr__(self) -> str:
output = f'<MessageOne: [{self.method_corr}, {self.selected_cipher} | {self.cipher_suites}, ' \
f'{EdhocMessage._truncate(self.g_x)}, {hexlify(self.conn_idi)}'
output = f'<MessageOne: [{self.method}, {self.selected_cipher} | {self.cipher_suites}, ' \
f'{EdhocMessage._truncate(self.g_x)}, {hexlify(self.c_i)}'
if self.aad1 != b'':
output += f'{hexlify(self.aad1)}'
output += ']>'
Expand Down
Loading