diff --git a/circuitmatter/__init__.py b/circuitmatter/__init__.py index 92dfbc1..38e99d8 100644 --- a/circuitmatter/__init__.py +++ b/circuitmatter/__init__.py @@ -10,12 +10,13 @@ import cryptography import ecdsa +from ecdsa import der from typing import Optional +from . import crypto from . import data_model from . import interaction_model -from . import session from . import tlv TEST_CERTS = pathlib.Path( @@ -351,7 +352,7 @@ def __init__(self, random_source, socket, local_session_id): self.session_active_threshold = None self.exchanges = {} - self._nonce = bytearray(session.CRYPTO_AEAD_NONCE_LENGTH_BYTES) + self._nonce = bytearray(crypto.AEAD_NONCE_LENGTH_BYTES) self.socket = socket self.node_ipaddress = None @@ -932,13 +933,37 @@ class NOCSRElements(tlv.Structure): # Skip vendor reserved +def encode_set(*encoded_pieces): + total_len = sum([len(p) for p in encoded_pieces]) + return b"\x31" + der.encode_length(total_len) + b"".join(encoded_pieces) + + +def encode_utf8_string(s): + encoded = s.encode("utf-8") + return b"\x0c" + der.encode_length(len(encoded)) + encoded + + class NodeOperationalCredentialsCluster(data_model.NodeOperationalCredentialsCluster): - def __init__(self): + def __init__(self, group_key_manager, mdns_server, port): + super().__init__() + + self.group_key_manager = group_key_manager + self.dac_key = ecdsa.keys.SigningKey.from_der( TEST_DAC_KEY_DER.read_bytes(), hashfunc=hashlib.sha256 ) self.new_key_for_update = False + self.pending_root_cert = None + self.pending_signing_key = None + + self.nocs = [] + self.fabrics = [] + self.commissioned_fabrics = 0 + self.supported_fabrics = 10 + + self.mdns_server = mdns_server + self.port = port def certificate_chain_request( self, @@ -984,8 +1009,8 @@ def attestation_request( return response def csr_request( - self, session, args: data_model.NodeOperationalCredentialsCluster.CsrRequest - ) -> data_model.NodeOperationalCredentialsCluster.CsrResponse: + self, session, args: data_model.NodeOperationalCredentialsCluster.CSRRequest + ) -> data_model.NodeOperationalCredentialsCluster.CSRResponse: # Section 6.4.6.1 # CSR stands for Certificate Signing Request. A NOCSR is a Node Operational Certificate Signing Request @@ -995,8 +1020,65 @@ def csr_request( # CSRNonce = tlv.OctetStringMember(0, 32) # IsForUpdateNOC = tlv.BoolMember(1, optional=True, default=False) + self.pending_signing_key = ecdsa.keys.SigningKey.generate( + curve=ecdsa.NIST256p, hashfunc=hashlib.sha256 + ) + + # DER encode the request + # https://www.rfc-editor.org/rfc/rfc2986 Section 4.2 + certification_request = [] + + certification_request_info = [] + + # Version + certification_request_info.append(der.encode_integer(0)) + + # subject + attribute_type = der.encode_oid(2, 5, 4, 10) + value = encode_utf8_string("CSA") + + subject = der.encode_sequence( + encode_set(der.encode_sequence(attribute_type, value)) + ) + certification_request_info.append(subject) + + # Subject Public Key Info + algorithm = der.encode_sequence( + der.encode_oid(1, 2, 840, 10045, 2, 1), + der.encode_oid(1, 2, 840, 10045, 3, 1, 7), + ) + self.pending_public_key = self.pending_signing_key.verifying_key.to_string( + encoding="uncompressed" + ) + public_key = der.encode_bitstring(self.pending_public_key, unused=0) + spki = der.encode_sequence(algorithm, public_key) + certification_request_info.append(spki) + + # Extensions + extension_request = der.encode_sequence( + der.encode_oid(1, 2, 840, 113549, 1, 9, 14), + encode_set(der.encode_sequence()), + ) + certification_request_info.append(der.encode_constructed(0, extension_request)) + + certification_request_info = der.encode_sequence(*certification_request_info) + certification_request.append(certification_request_info) + + signature_algorithm = der.encode_sequence( + der.encode_oid(1, 2, 840, 10045, 4, 3, 2) + ) + certification_request.append(signature_algorithm) + + # Signature + signature = self.pending_signing_key.sign_deterministic( + certification_request_info, + hashfunc=hashlib.sha256, + sigencode=ecdsa.util.sigencode_der_canonize, + ) + certification_request.append(der.encode_bitstring(signature, unused=0)) + # Generate a new key pair. - new_key_csr = b"TODO" + new_key_csr = der.encode_sequence(*certification_request) # Create a CSR to reply back with. Sign it with the new private key. elements = NOCSRElements() @@ -1008,13 +1090,115 @@ def csr_request( # class CSRResponse(tlv.Structure): # NOCSRElements = tlv.OctetStringMember(0, RESP_MAX) # AttestationSignature = tlv.OctetStringMember(1, 64) - response = data_model.NodeOperationalCredentialsCluster.CsrResponse() + response = data_model.NodeOperationalCredentialsCluster.CSRResponse() response.NOCSRElements = elements response.AttestationSignature = self.dac_key.sign_deterministic( nocsr_tbs, hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_string ) return response + def add_trusted_root_certificate( + self, + session, + args: data_model.NodeOperationalCredentialsCluster.AddTrustedRootCertificate, + ) -> interaction_model.StatusCode: + self.pending_root_cert = args.RootCACertificate + return interaction_model.StatusCode.SUCCESS + + def add_noc( + self, session, args: data_model.NodeOperationalCredentialsCluster.AddNOC + ) -> data_model.NodeOperationalCredentialsCluster.NOCResponse: + # Section 11.18.6.8 + noc, _ = crypto.MatterCertificate.decode( + args.NOCValue[0], memoryview(args.NOCValue)[1:] + ) + icac, _ = crypto.MatterCertificate.decode( + args.ICACValue[0], memoryview(args.ICACValue)[1:] + ) + + response = data_model.NodeOperationalCredentialsCluster.NOCResponse() + + if noc.ec_pub_key != self.pending_public_key: + print(noc.ec_pub_key, self.pending_public_key) + response.StatusCode = ( + data_model.NodeOperationalCertStatusEnum.INVALID_PUBLIC_KEY + ) + return response + + # Save info about the fabric. + new_fabric_index = len(self.fabrics) + if new_fabric_index >= self.supported_fabrics: + response.StatusCode = data_model.NodeOperationalCertStatusEnum.TABLE_FULL + return response + + session.local_fabric_index = new_fabric_index + + # Store the NOC. + noc_struct = data_model.NodeOperationalCredentialsCluster.NOCStruct() + noc_struct.NOC = args.NOCValue + noc_struct.ICAC = args.ICACValue + self.nocs.append(noc_struct) + + # Store the fabric + new_fabric = ( + data_model.NodeOperationalCredentialsCluster.FabricDescriptorStruct() + ) + new_fabric.RootPublicKey = self.pending_root_cert + new_fabric.VendorID = args.AdminVendorId + new_fabric.FabricID = noc.subject.matter_fabric_id + new_fabric.NodeID = noc.subject.matter_node_id + self.fabrics.append(new_fabric) + + new_group_key = data_model.GroupKeyManagementCluster.KeySetWrite() + key_set = data_model.GroupKeySetStruct() + key_set.GroupKeySetID = 0 + key_set.GroupKeySecurityPolicy = ( + data_model.GroupKeySetSecurityPolicyEnum.TRUST_FIRST + ) + key_set.EpochKey0 = args.IPKValue + key_set.EpochStartTime0 = 0 + + new_group_key.GroupKeySet = key_set + self.group_key_manager.key_set_write(session, new_group_key) + + self.commissioned_fabrics += 1 + + # Get the root cert public key so we can create the compressed fabric id. + root_cert, _ = crypto.MatterCertificate.decode( + self.pending_root_cert[0], memoryview(self.pending_root_cert)[1:] + ) + fabric_id = struct.pack(">Q", noc.subject.matter_fabric_id) + compressed_fabric_id = ( + crypto.KDF(root_cert.ec_pub_key[1:], fabric_id, b"CompressedFabric", 8)[:8] + .hex() + .upper() + ) + node_id = struct.pack(">Q", new_fabric.NodeID).hex().upper() + instance_name = f"{compressed_fabric_id}-{node_id}" + self.mdns_server.advertise_service( + "_matter", + "_tcp", + self.port, + instance_name=instance_name, + subtypes=[ + f"_I{compressed_fabric_id}._sub._matter._tcp", + ], + ) + + response.StatusCode = data_model.NodeOperationalCertStatusEnum.OK + return response + + +class GroupKeyManagementCluster(data_model.GroupKeyManagementCluster): + def __init__(self): + super().__init__() + self.key_sets = [] + + def key_set_write( + self, session, args: data_model.GroupKeyManagementCluster.KeySetWrite + ) -> interaction_model.StatusCode: + return interaction_model.StatusCode.SUCCESS + class CircuitMatter: def __init__( @@ -1066,12 +1250,16 @@ def __init__( basic_info.vendor_id = vendor_id basic_info.product_id = product_id self.add_cluster(0, basic_info) + group_keys = GroupKeyManagementCluster() + self.add_cluster(0, group_keys) network_info = data_model.NetworkCommissioningCluster() network_info.connect_max_time_seconds = 10 self.add_cluster(0, network_info) general_commissioning = GeneralCommissioningCluster() self.add_cluster(0, general_commissioning) - noc = NodeOperationalCredentialsCluster() + noc = NodeOperationalCredentialsCluster( + group_keys, self.mdns_server, self.UDP_PORT + ) self.add_cluster(0, noc) def start_commissioning(self): @@ -1134,21 +1322,24 @@ def invoke(self, session, cluster, path, fields, command_ref): print("invoke", path) response = interaction_model.InvokeResponseIB() cdata = cluster.invoke(session, path, fields) - if cdata is None: + if isinstance(cdata, interaction_model.CommandDataIB): + if command_ref is not None: + cdata.CommandRef = command_ref + response.Command = cdata + else: cstatus = interaction_model.CommandStatusIB() cstatus.CommandPath = path status = interaction_model.StatusIB() - status.Status = interaction_model.StatusCode.UNSUPPORTED_COMMAND + if cdata is None: + status.Status = interaction_model.StatusCode.UNSUPPORTED_COMMAND + else: + status.Status = cdata cstatus.Status = status if command_ref is not None: cstatus.CommandRef = command_ref response.Status = cstatus return response - if command_ref is not None: - cdata.CommandRef = command_ref - print("cdata", cdata) - response.Command = cdata return response def process_packet(self, address, data): diff --git a/circuitmatter/__main__.py b/circuitmatter/__main__.py index 3ba33ac..72bd63b 100644 --- a/circuitmatter/__main__.py +++ b/circuitmatter/__main__.py @@ -36,15 +36,15 @@ def recvfrom_into(self, buffer, nbytes=None): def sendto(self, data, address): if address is None: raise ValueError("Address must be set") - direction, _, address, data_b64 = self.replay_data.pop(0) - if direction == "send": - decoded = binascii.a2b_base64(data_b64) - for i, b in enumerate(data): - if b != decoded[i]: - print("sent", data.hex(" ")) - print("old ", decoded.hex(" ")) - print(i, hex(b), hex(decoded[i])) - raise RuntimeError("Next replay packet does not match sent data") + # direction, _, address, data_b64 = self.replay_data.pop(0) + # if direction == "send": + # decoded = binascii.a2b_base64(data_b64) + # for i, b in enumerate(data): + # if b != decoded[i]: + # # print("sent", data.hex(" ")) + # # print("old ", decoded.hex(" ")) + # # print(i, hex(b), hex(decoded[i])) + # print("Next replay packet does not match sent data") return len(data) @@ -111,6 +111,8 @@ def advertise_service( subtypes=[], instance_name="", ): + for active_service in self.active_services.values(): + active_service.kill() subtypes = [f"--subtype={subtype}" for subtype in subtypes] txt_records = [f"{key}={value}" for key, value in txt_records.items()] if service_type in self.active_services: diff --git a/circuitmatter/crypto.py b/circuitmatter/crypto.py new file mode 100644 index 0000000..e3c829c --- /dev/null +++ b/circuitmatter/crypto.py @@ -0,0 +1,153 @@ +from . import tlv + +import enum +import hashlib +import hmac +import struct + +# Section 3.6 + +SYMMETRIC_KEY_LENGTH_BITS = 128 +SYMMETRIC_KEY_LENGTH_BYTES = 16 +AEAD_MIC_LENGTH_BITS = 128 +AEAD_MIC_LENGTH_BYTES = 16 +AEAD_NONCE_LENGTH_BYTES = 13 + +GROUP_SIZE_BITS = 256 +GROUP_SIZE_BYTES = 32 +PUBLIC_KEY_SIZE_BYTES = (2 * GROUP_SIZE_BYTES) + 1 + +HASH_LEN_BITS = 256 +HASH_LEN_BYTES = 32 +HASH_BLOCK_LEN_BYTES = 64 + + +class DNAttribute(tlv.List): + # Section 6.5.6.1 + + common_name = tlv.UTF8StringMember(1, 100) + surname = tlv.UTF8StringMember(2, 100) + serial_num = tlv.UTF8StringMember(3, 100) + country_name = tlv.UTF8StringMember(4, 100) + locality_name = tlv.UTF8StringMember(5, 100) + state_or_province_name = tlv.UTF8StringMember(6, 100) + org_name = tlv.UTF8StringMember(7, 100) + org_unit_name = tlv.UTF8StringMember(8, 100) + title = tlv.UTF8StringMember(9, 100) + name = tlv.UTF8StringMember(10, 100) + given_name = tlv.UTF8StringMember(11, 100) + initials = tlv.UTF8StringMember(12, 100) + gen_qualifier = tlv.UTF8StringMember(13, 100) + dn_qualifier = tlv.UTF8StringMember(14, 100) + pseudonym = tlv.UTF8StringMember(15, 100) + domain_component = tlv.OctetStringMember(16, 100) + matter_node_id = tlv.IntMember(17, signed=False, octets=8) + matter_firmware_signing_id = tlv.IntMember(18, signed=False, octets=8) + matter_icac_id = tlv.IntMember(19, signed=False, octets=8) + matter_rcac_id = tlv.IntMember(20, signed=False, octets=8) + matter_fabric_id = tlv.IntMember(21, signed=False, octets=8) + matter_noc_cat = tlv.IntMember(22, signed=False, octets=8) + common_name_ps = tlv.OctetStringMember(129, 100) + surname_ps = tlv.OctetStringMember(130, 100) + serial_num_ps = tlv.OctetStringMember(131, 100) + country_name_ps = tlv.OctetStringMember(132, 100) + locality_name_ps = tlv.OctetStringMember(133, 100) + state_or_province_name_ps = tlv.OctetStringMember(134, 100) + org_name_ps = tlv.OctetStringMember(135, 100) + org_unit_name_ps = tlv.OctetStringMember(136, 100) + title_ps = tlv.OctetStringMember(137, 100) + name_ps = tlv.OctetStringMember(138, 100) + given_name_ps = tlv.OctetStringMember(139, 100) + initials_ps = tlv.OctetStringMember(140, 100) + gen_qualifier_ps = tlv.OctetStringMember(141, 100) + dn_qualifier_ps = tlv.OctetStringMember(142, 100) + pseudonym_ps = tlv.OctetStringMember(143, 100) + + +class BasicContraints(tlv.Structure): + # Section 6.5.11.1 + is_ca = tlv.BoolMember(1) + path_len_constraint = tlv.IntMember(2, signed=False, octets=1, optional=True) + + +class Extensions(tlv.List): + # Section 6.5.11 + basic_cnstr = tlv.StructMember(1, BasicContraints) + key_usage = tlv.IntMember(2, signed=False, octets=2) + extended_key_usage = tlv.ArrayMember( + 3, tlv.IntMember(None, signed=False, octets=1), max_length=100 + ) + subject_key_id = tlv.OctetStringMember(4, 20) + authority_key_id = tlv.OctetStringMember(5, 20) + future_extension = tlv.OctetStringMember(6, 400) + + +class SignatureAlgorithm(enum.IntEnum): + # Section 6.5.5 + ECDSA_WITH_SHA256 = 1 + + +class PublicKeyAlgorithm(enum.IntEnum): + # Section 6.5.8 + EC_PUB_KEY = 1 + + +class EllipticCurveId(enum.IntEnum): + # Section 6.5.9 + PRIME256V1 = 1 + + +class MatterCertificate(tlv.Structure): + # Section 6.5.2 + + serial_num = tlv.OctetStringMember(1, 20) + sig_algo = tlv.EnumMember(2, SignatureAlgorithm) + issuer = tlv.ListMember(3, DNAttribute) + not_before = tlv.IntMember(4, signed=False, octets=4) + not_after = tlv.IntMember(5, signed=False, octets=4) + subject = tlv.ListMember(6, DNAttribute) + pub_key_algo = tlv.EnumMember(7, PublicKeyAlgorithm) + ec_curve_id = tlv.EnumMember(8, EllipticCurveId) + ec_pub_key = tlv.OctetStringMember(9, 65) + extensions = tlv.ListMember(10, Extensions) + signature = tlv.OctetStringMember(11, GROUP_SIZE_BYTES * 2) + + +def Hash(message) -> bytes: + return hashlib.sha256(message).digest() + + +def HMAC(key, message) -> bytes: + m = hmac.new(key, digestmod=hashlib.sha256) + m.update(message) + return m.digest() + + +def HKDF_Extract(salt, input_key) -> bytes: + return HMAC(salt, input_key) + + +def HKDF_Expand(prk, info, length) -> bytes: + if length > 255: + raise ValueError("length must be less than 256") + last_hash = b"" + bytes_generated = [] + num_bytes_generated = 0 + i = 1 + while num_bytes_generated < length: + num_bytes_generated += HASH_LEN_BYTES + # Do the hmac directly so we don't need to allocate a buffer for last_hash + info + i. + m = hmac.new(prk, digestmod=hashlib.sha256) + m.update(last_hash) + m.update(info) + m.update(struct.pack("b", i)) + last_hash = m.digest() + bytes_generated.append(last_hash) + i += 1 + return b"".join(bytes_generated) + + +def KDF(input_key, salt, info, length): + if salt is None: + salt = b"\x00" * HASH_LEN_BYTES + return HKDF_Expand(HKDF_Extract(salt, input_key), info, length / 8) diff --git a/circuitmatter/data_model.py b/circuitmatter/data_model.py index 9ab07a5..ee9c5fd 100644 --- a/circuitmatter/data_model.py +++ b/circuitmatter/data_model.py @@ -1,7 +1,7 @@ import enum import random import struct -from typing import Iterable, Optional +from typing import Iterable, Union from . import interaction_model from . import tlv @@ -174,7 +174,7 @@ def _commands(cls) -> Iterable[tuple[str, Command]]: def invoke( self, session, path, fields - ) -> Optional[interaction_model.CommandDataIB]: + ) -> Union[interaction_model.CommandDataIB, interaction_model.StatusCode, None]: found = False for field_name, descriptor in self._commands(): if descriptor.command_id != path.Command: @@ -190,15 +190,18 @@ def invoke( print(field_name, "not implemented") return None print("result", result) - cdata = interaction_model.CommandDataIB() - response_path = interaction_model.CommandPathIB() - response_path.Endpoint = path.Endpoint - response_path.Cluster = path.Cluster - response_path.Command = descriptor.response_id - cdata.CommandPath = response_path - if result: - cdata.CommandFields = descriptor.response_type.encode(result) - return cdata + if descriptor.response_type is not None: + cdata = interaction_model.CommandDataIB() + response_path = interaction_model.CommandPathIB() + response_path.Endpoint = path.Endpoint + response_path.Cluster = path.Cluster + response_path.Command = descriptor.response_id + cdata.CommandPath = response_path + if result: + cdata.CommandFields = descriptor.response_type.encode(result) + return cdata + else: + return result if not found: print("not found", path.Command) return None @@ -277,6 +280,42 @@ class ProductAppearance(tlv.Structure): max_paths_per_invoke = NumberAttribute(0x16, signed=False, bits=16, default=1) +class GroupKeySetSecurityPolicyEnum(Enum8): + TRUST_FIRST = 0 + CACHE_AND_SYNC = 1 + + +class GroupKeyMulticastPolicyEnum(Enum8): + PER_GROUP_ID = 0 + ALL_NODES = 1 + + +class GroupKeySetStruct(tlv.Structure): + GroupKeySetID = tlv.IntMember(0, signed=False, octets=2) + GroupKeySecurityPolicy = tlv.EnumMember(1, GroupKeySetSecurityPolicyEnum) + EpochKey0 = tlv.OctetStringMember(2, 16) + EpochStartTime0 = tlv.IntMember(3, signed=False, octets=8) + EpochKey1 = tlv.OctetStringMember(4, 16) + EpochStartTime1 = tlv.IntMember(5, signed=False, octets=8) + EpochKey2 = tlv.OctetStringMember(6, 16) + EpochStartTime2 = tlv.IntMember(7, signed=False, octets=8) + GroupKeyMulticastPolicy = tlv.EnumMember(8, GroupKeyMulticastPolicyEnum) + + +class GroupKeyManagementCluster(Cluster): + CLUSTER_ID = 0x3F + + class KeySetWrite(tlv.Structure): + GroupKeySet = tlv.StructMember(0, GroupKeySetStruct) + + group_key_map = ListAttribute(0) + group_table = ListAttribute(1) + max_groups_per_fabric = NumberAttribute(2, signed=False, bits=16, default=0) + max_group_keys_per_fabric = NumberAttribute(3, signed=False, bits=16, default=1) + + key_set_write = Command(0, KeySetWrite, None, None) + + class CommissioningErrorEnum(Enum8): OK = 0 VALUE_OUTSIDE_RANGE = 1 @@ -438,7 +477,7 @@ class NOCStruct(tlv.Structure): class FabricDescriptorStruct(tlv.Structure): RootPublicKey = tlv.OctetStringMember(1, 65) VendorID = tlv.IntMember(2, signed=False, octets=2) - FabricID = tlv.IntMember(3, signed=False, octets=2) + FabricID = tlv.IntMember(3, signed=False, octets=8) NodeID = tlv.IntMember(4, signed=False, octets=8) Label = tlv.UTF8StringMember(5, max_length=32, default="") @@ -488,6 +527,13 @@ class RemoveFabric(tlv.Structure): class AddTrustedRootCertificate(tlv.Structure): RootCACertificate = tlv.OctetStringMember(0, 400) + nocs = ListAttribute(0) + fabrics = ListAttribute(1) + supported_fabrics = NumberAttribute(2, signed=False, bits=8) + commissioned_fabrics = NumberAttribute(3, signed=False, bits=8) + trusted_root_certificates = ListAttribute(4) + current_fabric_index = NumberAttribute(5, signed=False, bits=8, default=0) + attestation_request = Command(0x00, AttestationRequest, 0x01, AttestationResponse) certificate_chain_request = Command( @@ -504,6 +550,4 @@ class AddTrustedRootCertificate(tlv.Structure): remove_fabric = Command(0x0A, RemoveFabric, 0x08, NOCResponse) - add_trusted_root_certificate = Command( - 0x0B, AddTrustedRootCertificate, 0x08, NOCResponse - ) + add_trusted_root_certificate = Command(0x0B, AddTrustedRootCertificate, None, None) diff --git a/circuitmatter/pase.py b/circuitmatter/pase.py index 76926e3..3c86d33 100644 --- a/circuitmatter/pase.py +++ b/circuitmatter/pase.py @@ -1,8 +1,8 @@ +from . import crypto from . import tlv from . import session import hashlib -import hmac import struct from cryptography.hazmat.primitives.ciphers.aead import AESCCM @@ -65,26 +65,17 @@ class PBKDFParamResponse(tlv.Structure): ) -CRYPTO_GROUP_SIZE_BITS = 256 -CRYPTO_GROUP_SIZE_BYTES = 32 -CRYPTO_PUBLIC_KEY_SIZE_BYTES = (2 * CRYPTO_GROUP_SIZE_BYTES) + 1 - -CRYPTO_HASH_LEN_BITS = 256 -CRYPTO_HASH_LEN_BYTES = 32 -CRYPTO_HASH_BLOCK_LEN_BYTES = 64 - - class PAKE1(tlv.Structure): - pA = tlv.OctetStringMember(1, CRYPTO_PUBLIC_KEY_SIZE_BYTES) + pA = tlv.OctetStringMember(1, crypto.PUBLIC_KEY_SIZE_BYTES) class PAKE2(tlv.Structure): - pB = tlv.OctetStringMember(1, CRYPTO_PUBLIC_KEY_SIZE_BYTES) - cB = tlv.OctetStringMember(2, CRYPTO_HASH_LEN_BYTES) + pB = tlv.OctetStringMember(1, crypto.PUBLIC_KEY_SIZE_BYTES) + cB = tlv.OctetStringMember(2, crypto.HASH_LEN_BYTES) class PAKE3(tlv.Structure): - cA = tlv.OctetStringMember(1, CRYPTO_HASH_LEN_BYTES) + cA = tlv.OctetStringMember(1, crypto.HASH_LEN_BYTES) M = PointJacobi.from_bytes( @@ -95,17 +86,17 @@ class PAKE3(tlv.Structure): NIST256p.curve, b"\x03\xd8\xbb\xd6\xc6\x39\xc6\x29\x37\xb0\x4d\x99\x7f\x38\xc3\x77\x07\x19\xc6\x29\xd7\x01\x4d\x49\xa2\x4b\x4f\x98\xba\xa1\x29\x2b\x49", ) -CRYPTO_W_SIZE_BYTES = CRYPTO_GROUP_SIZE_BYTES + 8 +crypto.W_SIZE_BYTES = crypto.GROUP_SIZE_BYTES + 8 # in the spake2p math P is NIST256p.generator # in the spake2p math p is NIST256p.order def _pbkdf2(passcode, salt, iterations): ws = hashlib.pbkdf2_hmac( - "sha256", struct.pack(" bytes: return tt -def Crypto_Hash(message) -> bytes: - return hashlib.sha256(message).digest() - - -def Crypto_HMAC(key, message) -> bytes: - m = hmac.new(key, digestmod=hashlib.sha256) - m.update(message) - return m.digest() - - -def HKDF_Extract(salt, input_key) -> bytes: - return Crypto_HMAC(salt, input_key) - - -def HKDF_Expand(prk, info, length) -> bytes: - if length > 255: - raise ValueError("length must be less than 256") - last_hash = b"" - bytes_generated = [] - num_bytes_generated = 0 - i = 1 - while num_bytes_generated < length: - num_bytes_generated += CRYPTO_HASH_LEN_BYTES - # Do the hmac directly so we don't need to allocate a buffer for last_hash + info + i. - m = hmac.new(prk, digestmod=hashlib.sha256) - m.update(last_hash) - m.update(info) - m.update(struct.pack("b", i)) - last_hash = m.digest() - bytes_generated.append(last_hash) - i += 1 - return b"".join(bytes_generated) - - -def Crypto_KDF(input_key, salt, info, length): - if salt is None: - salt = b"\x00" * CRYPTO_HASH_LEN_BYTES - return HKDF_Expand(HKDF_Extract(salt, input_key), info, length / 8) - - def KDF(salt, key, info): # Section 3.10 defines the mapping from KDF to Crypto_KDF but it is wrong! # The arg order is correct above. - return Crypto_KDF(key, salt, info, CRYPTO_HASH_LEN_BITS) + return crypto.KDF(key, salt, info, crypto.HASH_LEN_BITS) def Crypto_P2(tt, pA, pB) -> tuple[bytes, bytes, bytes]: - KaKe = Crypto_Hash(tt) - Ka = KaKe[: CRYPTO_HASH_LEN_BYTES // 2] - Ke = KaKe[CRYPTO_HASH_LEN_BYTES // 2 :] + KaKe = crypto.Hash(tt) + Ka = KaKe[: crypto.HASH_LEN_BYTES // 2] + Ke = KaKe[crypto.HASH_LEN_BYTES // 2 :] # https://github.com/project-chip/connectedhomeip/blob/c88d5cf83cd3e3323ac196630acc34f196a2f405/src/crypto/CHIPCryptoPAL.cpp#L458-L468 KcAKcB = KDF(None, Ka, b"ConfirmationKeys") - KcA = KcAKcB[: CRYPTO_HASH_LEN_BYTES // 2] - KcB = KcAKcB[CRYPTO_HASH_LEN_BYTES // 2 :] - cA = Crypto_HMAC(KcA, pB) - cB = Crypto_HMAC(KcB, pA) + KcA = KcAKcB[: crypto.HASH_LEN_BYTES // 2] + KcB = KcAKcB[crypto.HASH_LEN_BYTES // 2 :] + cA = crypto.HMAC(KcA, pB) + cB = crypto.HMAC(KcB, pA) return (cA, cB, Ke) def compute_session_keys(Ke, secure_session_context): - keys = Crypto_KDF( + keys = crypto.KDF( Ke, b"", b"SessionKeys", - 3 * session.CRYPTO_SYMMETRIC_KEY_LENGTH_BITS, + 3 * crypto.SYMMETRIC_KEY_LENGTH_BITS, ) - secure_session_context.i2r_key = keys[: session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES] + secure_session_context.i2r_key = keys[: crypto.SYMMETRIC_KEY_LENGTH_BYTES] secure_session_context.i2r = AESCCM( secure_session_context.i2r_key, - tag_length=session.CRYPTO_AEAD_MIC_LENGTH_BYTES, + tag_length=crypto.AEAD_MIC_LENGTH_BYTES, ) secure_session_context.r2i_key = keys[ - session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES : 2 - * session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES + crypto.SYMMETRIC_KEY_LENGTH_BYTES : 2 * crypto.SYMMETRIC_KEY_LENGTH_BYTES ] secure_session_context.r2i = AESCCM( secure_session_context.r2i_key, - tag_length=session.CRYPTO_AEAD_MIC_LENGTH_BYTES, + tag_length=crypto.AEAD_MIC_LENGTH_BYTES, ) secure_session_context.attestation_challenge = keys[ - 2 * session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES : 3 - * session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES + 2 * crypto.SYMMETRIC_KEY_LENGTH_BYTES : 3 * crypto.SYMMETRIC_KEY_LENGTH_BYTES ] def compute_verification(random_source, pake1, pake2, context, verifier): - w0 = memoryview(verifier)[:CRYPTO_GROUP_SIZE_BYTES] - L = memoryview(verifier)[CRYPTO_GROUP_SIZE_BYTES:] + w0 = memoryview(verifier)[: crypto.GROUP_SIZE_BYTES] + L = memoryview(verifier)[crypto.GROUP_SIZE_BYTES :] L = Point.from_bytes(NIST256p.curve, L) w0 = int.from_bytes(w0, byteorder="big") y, Y = Crypto_pB(random_source, w0, L) diff --git a/circuitmatter/session.py b/circuitmatter/session.py index a6c6a57..919fa88 100644 --- a/circuitmatter/session.py +++ b/circuitmatter/session.py @@ -1,13 +1,5 @@ from . import tlv -# Section 3.6 - -CRYPTO_SYMMETRIC_KEY_LENGTH_BITS = 128 -CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES = 16 -CRYPTO_AEAD_MIC_LENGTH_BITS = 128 -CRYPTO_AEAD_MIC_LENGTH_BYTES = 16 -CRYPTO_AEAD_NONCE_LENGTH_BYTES = 13 - class SessionParameterStruct(tlv.Structure): session_idle_interval = tlv.IntMember(1, signed=False, octets=4, optional=True) diff --git a/test_data/recorded_packets.jsonl b/test_data/recorded_packets.jsonl index acaffb6..fff8a20 100644 --- a/test_data/recorded_packets.jsonl +++ b/test_data/recorded_packets.jsonl @@ -1,34 +1,46 @@ -["urandom", 784666670915434, 4, "gTyCfg=="] -["urandom", 784666670932045, 4, "WEtNXg=="] -["urandom", 784666670944279, 4, "Im0D9Q=="] -["urandom", 784666670954137, 4, "q3+7MQ=="] -["urandom", 784666670967192, 8, "GdHmWrzZGms="] -["receive", 784671745464646, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "BAAAANR1RwbAzDLm0ZLOIQUgmg0AABUwASD/PukDdOXeqJ1IBdpHgqsgOMm3wxKzsceqrEXbVN2XnyUCvzMkAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="] -["urandom", 784671771475666, 32, "R9etlJW4WJ2hI+MXNURAUgqg0EAvyRTyMrcduh1jOSE="] -["urandom", 784671771496185, 4, "SqSOEg=="] -["send", 784671771593809, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AQAAAMoj6AfAzDLm0ZLOIQIhmg0AANR1RwYVMAEg/z7pA3Tl3qidSAXaR4KrIDjJt8MSs7HHqqxF21Tdl58wAiBH162UlbhYnaEj4xc1REBSCqDQQC/JFPIytx26HWM5ISQDATUEJQEQJzACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] -["receive", 784671777601546, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "BAAAANV1RwbAzDLm0ZLOIQUimg0AABUwAUEEEqRKQ9IIhbFzmFRT5DujfzWbge9+63W++o8ir/oUfxBKkLElBbBaT+n96w7pMk6pxi/JfmJwouj3RjZYy4O4MRg="] -["randbelow", 784671777703077, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 26649162366983277512998315276067044341880723378171385531045348526206345387847] -["send", 784671787820468, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AQAAAMsj6AfAzDLm0ZLOIQIjmg0AANV1RwYVMAFBBMSdK7qIGwnaU3L8sov92kGKTqOhK56kBKAj55TYu3mZ9mLXCQrXPeZ/38c/YMxeIJ3EAfN/bOR6ec35cqwV0yAwAiAiuC1nE3Ubp++vlT/OYtqaqGoFdt/Usottde5xkgwMShg="] -["receive", 784671788261229, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "BAAAANZ1RwbAzDLm0ZLOIQUkmg0AABUwASCsVcEv3Eh8OPfsmxLISxCVu7qwCbMN7+LL3jXTvSIkkRg="] -["send", 784671788334538, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AQAAAMwj6AfAzDLm0ZLOIQJAmg0AANZ1RwYAAAAAAAAAAA=="] -["receive", 784671788501062, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJPvywjQvaLnorG7I/AQoh4IJIg9SEd3owBKpVZd2KNZwHF5nlhQ8BhHzr/wpTFRUXtHo0kznWwiReoKTEnjb2e0lKSaGoAP5UADc8o+/IaJl5rvUneuijc9G9HIpcH4faBYMPQiOustDPLjUyDGobn/WYmPs29V1fle1Mizu5Y="] -["send", 784671789179181, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEbqKAGkyTFFEYXsJR7xYOlDV6nG3ES3MXEEYJHxjkfLv/u19GYfxnFvWDylvm+NbKnprXNebZiVdveov7t1fuaYovmxjv+nZ0RfQBkrabKLERGg5VKxeVoQxFYuIhqRj4iaXJAC3PQjmFA9Lrw6t3VNgcmG1yHa1fY5210krbhRPkxSPBFkbFxRDz+ige2uvRTzdANY1tI5ZhfiYYfC3LwaJsuuSuxhwIXbFoE/5qu8Zp/nAKGZme5PtrBhZcJDMOjKcsfK1Qgyn4kDTUiUMBSIBB2QGuoUf0db6R44fbcNlVvShYI="] -["receive", 784671790132390, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJTvywgs9CHUeFUMwzTra9wAscaZOCzTg6O62BGTYcVAuHo="] -["receive", 784671790226888, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJXvywiLBDIrxesC/ZX4yeRwt9gndvFwbE8ip2lWnOCxh1u0yv3FKPUVW6+0Ks1uqo6aOJ9FLvGHeyidHLhA2wL1yXSikgvl6s8PDVoniSPXSg=="] -["send", 784671790449899, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEfqKAGsoFJIDBcl+c0GSsGQHaX5ZSkQLeprcL862VIIkH7a35ywtoJCtDFEOEJISHst2s0HB8NBkg=="] -["receive", 784671794339509, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJbvywiVXApjl2O2rCpEp6tCMdZsxXJ8Fg9hz1I3wbfwk3Y="] -["receive", 784671794460868, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJfvywhxaDeamkfBt6JM14rzvD6822cZ8/A2oFzI/B4cjRr+rVN4Xo6BJ/4XSWtexshQvDyDgAfufoyfaBs="] -["send", 784671794721990, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEjqKAEuoNxQBRLD1VXDDfxtRJk9II/gIrTGAtPcUPEC4bSY+8oIfHmiCkjjl3r9E3ESLv8LaAMp4ncUqorFrA=="] -["receive", 784671795072291, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJjvywjZRYWm/kdLVbc/vtoKHymQDD1edYM1hHd3rU60iykpJB/Mr2hDRYYc9/zgJcqL/DlpqUaLEpXrJlEYN4teYA=="] -["send", 784671795307716, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEnqKAHlAQA5+esg7SoD5Jcsx413iVa2qW5mPR765RLosKM15P7MhfPU6VISFU4N6Wi7ejm4wQp2bIlXPItFdg=="] -["receive", 784671795554070, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJnvywhA/bnhfPrxP7AHJa6AbuGIQ2/rp1frP/aWI5lqQp2y33YL++YHMZPV9cZ/jaPoFEXltq1LaAw="] -["send", 784671795904602, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAErqKAGDdc+1QQxQeLDWjhtSBX88F+bdVCnUkQ07c2psS1hMwTbbF592r6YncrDegkKmGp4o8gEHccr5MYAccNlKo+6GHx6m3i7mUOd9uYAFQ/8HylwXl+/2JoKGJdBjHQn34p322B4v6i7EEdr6t59W/UYSKz8cUtKr11rNPbI/w8u3teOHPmfCAo3WhOkjYnbBodSeoGvCX5Bwi1WB7xtyVVvcWZfm8d66Elq6ECjuWJk2bRUzfSuem6+l+kvwE6SbCEsUswz8Ny6LeUy9F0mISWFSzHxFKteKlFwjUUtV+WNVIqKPOlDCas39NkoyCl9+jBKc2CYdLQ2ifbrizGoQ3Q7hckAmpcl2Di/O0p/GOmOt/x9HIUnrPI6i3NlLKQYL9TVXwI9bUwGq5pbxtmP/TeiU3y2CEX8gEjrKMV7PcY7IsSuO2WQIHh2x/AT/dKdPYqQ1Il6tgQnnXJZ/5C9JYQBiBL1xmUTNKt4vmUtuW3uW7B/r8/ICkiZGyBpyvN+l+7lfRJGAs/riIbgFbUPdKrQfOpxvKFaD1S1HzD2Dc92X5f37j4tgBs4xerod9bBbIWRH+3aARmSHsoL8vPay86HHUAj+z6z1sI/ZyOGkThf9UDcIr0d3hKw0TltboG90Rtycrs0pOJTmZ2kKE5epzhbV3urjnVAESeeIAC94NR+Z9PeNkUq5djHMw8qdHsFukwQW"] -["receive", 784671796157078, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJrvywgWRzA9jTjmdG0PHw4W34ADp1z14TYE0VAdelkqjoH6pCdbjbk+Woe4GAkq8nuV/8Dv1qg25Ws="] -["send", 784671796492802, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEvqKAGSmI/Hf2oUaTztMFwWf1Wr7UzcVAw00fbDY5I5JZv0GekEl89ttbbqxiqX01LOvqmaegYEyB5IR9xLTN2qV1DHvfLfVxxeRTh/w1RnVondC9DU+Tg2NDL+d0modgJy+mtrvANUaIjKVy/80J5LjG//NMP1aw61S3dr+eqp7ClE/rIFU3qwC4Gkk9M78XnzyTQTJ7Iwke6CmPkFaSAZRkCmRi1ps1x58InVKXdzz2h9Fj/9meP+BfJMgbQ57+XCTYyBKZsaDNyRevHdBXdVfFvYOARiPXtg5GiZQtGBBLsf8Ucrguo0CMKSxwfMWK82OPFoquGE8Dg23tqU4aCRBYK3Me6P94cU7QS/Ikg6zJmofCFbsS5VRjVljxxBrLcRRjC+40DUTkb20LR58421FZR8CbPUSAFoe7tG1nrg75OL4/4o2BGr5wC2s1XDfwq9noxvNNum+vQnRsdmS95yypSO+9QZp5Fy7dRQjLK6JMRRwwqUyN4AhnV8uOLhsZ+nhgBBmiJ0ZSV4yM3OYme11iuLFvoeeM2nVNXobfFXfXqItOui7Fwtm69rMUyXV1jEpPuWIJV5IVWTYp2XrtqhLVOgBgP7xte0dlZOalCcBXGqFLEwz5TYiTMVBpZmTYHX+bPrXnfpfGHMuuKt/TNtYYxbPFxhb6EwpDT2TYZQweh5mAvP5y+69WA5wuKaPlioGR4IE8vIbs9Pe8waBNb9YHH3jjP9UTEQ"] -["receive", 784671796763933, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJvvywhXIkAFnDC5PCH22NnVWxSNyPNJq7RYhdW518I67TbrGubnXwiOpqJ/spqjy31aVfGAyTGZb0yuROzcPBU9HCDtTNZ2PdygZwCgERNPgsFKu1bzgEUc3g=="] -["send", 784671797646439, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEzqKAGZCnaekdC8CSrDLJFqRBq+EUvWc9/SlYjbcCyf4hxYXajISL9LOh6m1PAngaYvF/xaGwiTNMRULgp7NUYK/QeqcbQWQz9mKgDcgwspqG0PzqerpemTMSNfrqDF4LTWG6xjHkHjgp8xDWSXgfFF7sjtwz77VhLUIAKdiY2rmI0YzL3kXaaNr9qsJb5D4tVK2hfInfKN8gokigctcoR03gNm3gcaGlH3X39VgiD/PXQSY0rFQgKEndL9ifhvCK32PZDJnW1NgL/kpHmgnoUdgy5widWEaxj1Al+sRqEEKxJgpf7GIbmF7Wg+tMFPAC/2hq6LGjg/lplXvqezDc0mfbS/x+3B+N8lYPd+n56TQ3WnKjEJX4FsZe6vT8QHUqt8eUFVvMsQvNuPcn7ITCeM80qXF4lEXgX3aOOn79jp3kjj8dWD6z1PsG+QSNI3Mm3QtUW8QIDwbvD5Io0bme/Ui9qfXJwweYIfzMfyrNLORlo0sKqGwVMX/nOwvYxXnvq3NXOzwGbx9o1b0WO8vqa4SRKeTXJWeswQ"] -["receive", 784671799529853, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJzvywh6llkCvigUZMBX8vDqbW1YJEOmGfMR7PGU9XI7EgIR+yHPqksJ0VLs3Qc86cFy2ZC7PbyTC1pbNBI8dAoYbl8BC9TfygOTYx+YA1pJc7LEhLaKih2DUQ=="] -["send", 784671799737184, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAE3qKAEjEkjttuN4wJXi4DEWpedfLtARZkkDdIDPRcIGT2CGQCDJF+ekuQwRMsHcQqYo9bmq3PKGnelEUw=="] -["receive", 784671799972889, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJ3vywjHE+kKkA4WvpHpLMVas8pnKJk/32kYtfqTDIpRK6GCg+WAoJYC29i5GwwqdR2YPbdFAhvn0UILgM0="] -["send", 784671800183566, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAE7qKAFiLxKjMFToUcwYnxa0BIcm5HfRzM7T//WcWiyZO3OPshiawMakEDQLqN8V3wVakown/waF8LP65Iujuw=="] +["urandom", 871507505707987, 4, "LYTFPw=="] +["urandom", 871507505724057, 4, "iY4sjA=="] +["urandom", 871507505736501, 4, "VGDJjA=="] +["urandom", 871507505744636, 4, "QuSDpw=="] +["urandom", 871507505760586, 8, "0PGaAxtzY2A="] +["receive", 871510714593623, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAMwUwQpGvSQkTU57tQUgS1gAABUwASCtkU/QXzijHlthCZ1aUI6G+OjmKqBQx5rWaOtXz1BIESUCh04kAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="] +["urandom", 871510741118477, 32, "ccncgbiV+n7AGVAQZjwSTlG+TIRHu3W2DbodunF9jdE="] +["urandom", 871510741140168, 4, "quQB6g=="] +["send", 871510741238353, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AQAAAERY/ANGvSQkTU57tQIhS1gAAMwUwQoVMAEgrZFP0F84ox5bYQmdWlCOhvjo5iqgUMea1mjrV89QSBEwAiBxydyBuJX6fsAZUBBmPBJOUb5MhEe7dbYNuh26cX2N0SQDATUEJQEQJzACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] +["receive", 871510747233066, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM0UwQpGvSQkTU57tQUiS1gAABUwAUEEV1RRuOlve0Df09KY83f47IzxiqUGFTZpEQD09obiPvcCjyOIhziiZ8W1cWHgCE4QUV/2H7yDRB0vDlaqXhvEeRg="] +["randbelow", 871510747348975, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 1372112454692769542481468369269276073942330241544482652254110676231987590818] +["send", 871510757117093, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AQAAAEVY/ANGvSQkTU57tQIjS1gAAM0UwQoVMAFBBJBlAvoBG5VgLXSdUtb3TwVwyNAp7T1EvRTKNU3nv4rv5KZtc1kaqYlQYkkpflmdMSYzYHp0k8RMlEkHXkie88MwAiAMY6gKq0gQezVlGFOhgvE2nsH7+QaTwTgDOPIpf7BhGRg="] +["receive", 871510757526795, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM4UwQpGvSQkTU57tQUkS1gAABUwASC09gAl9bruVi76LrlzQ+ybbYWti96RIl17zKb7cO4avBg="] +["send", 871510757606135, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AQAAAEZY/ANGvSQkTU57tQJAS1gAAM4UwQoAAAAAAAAAAA=="] +["receive", 871510757782357, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAN20VAGHsmD/rsF9BopTAFGhuQRhb+KBACHcwjhOztcLSGFIX7QSOUjpjYL2sRJRMpaYdoYeTriA/vhAlFMLOcivtVUP5LlheC03A2PJHp5fEypPdHkiTndhVGfk/qVf2Fx1dqdpQQDd6frYWP14J6gZhy2I9UYoEhDHiwi+lsE="] +["send", 871510758476055, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAEweoA4lVjKSA24mtNOw6eiRg7enlc1buKPr+8I2xCnewl7l1Y3uNxj0hZ787HWQdYkzPWbI/eKQDKAfT6hGrv+cIL7etjf+IEeA1kvEs++Fa8FK00ndAtIIlL79+u/4uVEqvcNaQMHUhC3FEyFiZ+u4SjVf4UDhWtkVdjnAn04e6Uk7JzfpGkR1tp3UEKv3cBlc749nzxvX6QQpaHFMtqY6D3BGVeVWffrqwwakEVoeoxBE6/6FPZOSrsjE4zwdJM1n+a9wHdhbeCwF1xFFNQyJ0rzGkqwk4P6viQL0Oo287TYJ0mY="] +["receive", 871510759404836, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAN60VAEJHWj3AEExQMElZbuNNvzahTZF6JIfUu/XKxFu0kw="] +["receive", 871510759500487, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAN+0VAE/TaX2/jtsVw1XhxSs2LX7uyxG+QYWt+gHMMQ3wK1dg2Q3j4oL+Rpi73UMr/csxk/mGA9Yo2gbWTwHoJc0jGiRxxdL8hTwTa+l5sFujg=="] +["send", 871510759738265, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAE0eoA6gySeKfHCuZ3uvuu2F43E9J71VFkGnKVVRtZvdHfGjzWdNgFd0uzbZmApNs0GjWyoG58fhQw=="] +["receive", 871510760001331, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOC0VAGkXHZzWisUrI5LQ38vo2kfoyG3uyzn5g/cTTp5qz4="] +["receive", 871510760102452, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOG0VAFACT6o+/nAZduzpi/uc7L5PTgxxZwUaAtv3DJmRIE8Z2Zgc09WT/7/BS9XYd+u5hBz2SuqY1MKNlM="] +["send", 871510760370297, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAE4eoA4FLe9QxePzbDzJW6fA9pcBjZmiQUTrFUwQpIkNRaDGuRuA4agq8riqVMtnwV+wD6N0vZNcUNxLT+usNg=="] +["receive", 871510760662057, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOK0VAGZpLer22Jco/RmWW37f1GCxvG4BC94UYQSB6Am8ZS3ax67xeodKK0pO1MEg9bdxeh1HL/G7eRHVXLV2wYIKQ=="] +["send", 871510760956863, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAE8eoA430KQCm/2QlvdZAeUNIFipPZf0+uPthKkiPwode3lXC7DCPdMQGFFvgum0errRC0h9AwSD4k8fCj+jjQ=="] +["receive", 871510761233805, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOO0VAExKRWtOgGCbgpgd78cO7rcxIOoZwmXGjeNZR4KGE1HnU2XCgz73CIUVo3Hnro9FSRz0vL9l6I="] +["send", 871510761593884, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFAeoA7QU5WNzHeYMdAJXWFAg3mqkaUtwC8HY5WAdtn7zy8hZP0JmecjVVL6OKfQdrqBIsFWAb2+ztLfvbn2pxrqLDaaWkqYNSQmUy4+roHylY5Yi/bDghMtw/uAp5eD0esliof0qvdEeCBJNxgVTnt9sW5oPrSJt8xGNyDbs2YV2ufPVNMKXPbcUr0k0Oo4HFkDLqeSbQPwMmW+3L/eV4nkiHGMlGoXRsQcleA8IoVXBo3dM/N+SNAaoziRwVfKfnZRuvmdkt57qmqemsvN9UEVfZl/PZQX/+pXaVPoO+7ulGOvs4nR7n3sy55uZA8XbVueYA8nPvCUJFQ2cnSye1h9GIc3xAeIQBVdVh3Phn9N0tZwVcwrRIzpnpEoJIqxgJq06vxWEJNboO8H/lW+VHAkooDvecq/gUh3EZViJ2SpnfJ1kJWWjXc2FV+LIaVzifwPJm/NbNDDvS6+1gRsoyjZsOHa5TGJtQWkDClPPg9HZP5YVnNwkFcFGt8n4MRfrYaHqgd5W7lWZSUC86a3i2B0pg4FTWIYpMRloR0R93DzKt4v7mSD6mzY+a1B3bLn0fo+NdgVeill8NodQ32fs0MarcVn4Dk15lEVkELswHw5tvpnMOIX7HNxtjinmvebwmHzbaHgnVxx2XXIIqq2ZVaEq5BIzApw0UsU1OyIPtwKKJHJgVEVFb+XCv65yZVwB4Yr8lcI"] +["receive", 871510761916101, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOS0VAG4u9A5duGe4zTqXeSDtC1dPUsjjFkMfiRhx7jL5OzRP7HeTnCn6KKvHyQDWKHGW7FM03uftps="] +["send", 871510762249089, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFEeoA5J3g7u/gjbmQ4vMlrrlojWEOoeIct/Q46/Qb7U0+2Bxogw/H36G6tPdjwm6WQmPXdxDi3/6BS6LhkAR7ZYyP4AXdBEsR0M3ErI9OIpXefieFGzOGRQZChgbdyVynfN+p9+b5asEnkznn0O80XUsoJavqMG071Exc3L2y/gQCuvqJUTLuxsUKQYKvR+tCn0Z96QcwFkITM+PQy00bsfeDYIz00ztGvJKhcoQnwz8SDljQ0VBUkAh+Cb1t50K5FNtoEitc1u7QfkMaRIcOq0/PiAS0tVPWyLWJ1+P0R9HWWkdpwJbt+lhYh0l3fIOnxYDLoiPTzXXxA3LiCwhhWozkz3MogR78w6jZlNugG4YtpwMZKiVFvJuUtQM3ZrfaADlPj4KNfP9xACalHBkbYMvAOEczp+LYSAqz7db7FZALZ6Z3tHb9gqSeZn9FRvsr33j5DxMNZ463FJtz4SLJRs1qCg+fj3NHHPVlEEr6v6IFhNol90N8k3C2HPeViu0zPne7qpTN8XLmcDwaIOsUqMo/pIU9xO2HRzIB9Z0W2oWKDYBNW1NQzC8qzo8iNpMThyw8xwAZaPSULS3iXcSZ9OssIXZhsnI2c9mHKrEnHHAK82SgLODw/Xo3Hew9pLoVlu8qH572kI5yN74GA5LFU+UMRfss8nGrZ1Y8ONbGb25xXHJXc9izimvEJtCCjSnJ+SDpvsrgsvxdjZDCu4IUwcDhcC8JUi3OiJ"] +["receive", 871510762616873, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOW0VAE6n+6ifjmAlz1XcgWVq4n42Em5+v5Q68VMnCxsbxfZRMIda0m9xQj5ACDvtzx5D3WpMwtUU1fR5e2KV/s0mFF3gYiOnEfoUtD7lGyZJtxGWGr2mvW1hg=="] +["send", 871510763454332, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFIeoA4sjyWsVHVw/cqQjFuHH/KrOkFLXQgUADVYqm5WjPnTiKFUh0WyQ2mgcd0aLcLqYyksMftwulxsjZWW1T1AR+Jn4m1BpdxVck2q8RGzDLTivtD3efPwMiOz0vb9uFoL4gUG2cxGif32XrY2hOL6ymVSOPbkpsF3rzez/qI0eiJIFEUM4d9Q+NALAo9qb7AIyN9WwqKvg9vv/cs2uDAL+xo4YaQRYA2MK8Ix41PytHq5gHOXCMYRCvsR1I1qt2frKjyr+8srV9c1evZZujz2g3ZEWMyq7vf6A8ITd3JGPRH9zM4UYIr1ej8tNII4htF+9RyLrO8jbO/ce5mH46kwj4g/+Ou2MBl0PuWJYqR47hONvFEIjCu6ygoVEnvcE3nyJ34RzjCn1pnTRir3clpatya6fU21eOlGQwPM9bVIHU54mM/OOAQwlv26YTcHvYXNssyOQ+V+7FjgpTuPXkUuIq0nq201iQY4Qs4wioHrgjJVTAO+BIDFTvGYyye6atcRKmE8N25B4g3GNUFfgQhwSpY8yg4bxhj9"] +["receive", 871510765694856, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOa0VAEblIYFdbSQgiGm6K9pubGrhB5cMn5tZHskzkukR2PuVz7m74+tiehJrsXvKm8UIv2tnSrgHyvo3+V2ebVnjwBzq8XYEMjVC3jk0yQv0xMWjeE5E5Bhjg=="] +["send", 871510767340209, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFMeoA7EKGToSc44Wi2zY+U/kGo+ThiPV+ZBE5D8MRHcynH532TJJxyQRVFKR0Vrfd1u6UVjQ+m1M1WzjXSsZwMLVpumdsj+mFcv2ZwJfL9iZ6cuXtZzZlTCjH6GQmBLGX3ju11ikadq+X1KxYEXJKJu5Q0lHll9w0DN9AhU3u2WrH0JQhwXL6dsx3UNShxkEvLLSLmr8JO7t3vShl+cE1dty6plEnYWOefVNB+lc+9ec7dPHWjZw7SGGN86gwuZuotYugPXoVsWjoNrduVYrAOZmJA9Wz0hA2SPIyCzpLDnp5ubUwjtTQVnGs/UKhl36cpK5eXuIJIATMb3E85fc47MPb7LZDLRrrQXzuqWolo2b7G0F/BDn7KAVWwvn4obNyUb1Gbg1eMv4qnbh5jIpdnJKx0ZtWmt4aQHjeAHkruyUgayE6hnKg5GcG+YgWQiSf9F9ceOmduGnJDMUpY/5lP+zcDPhQvDe9KPZn0bF8hyo7hkR6i2nKZ3CIkAQa4Yo5dC7LbW"] +["receive", 871510768445232, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOe0VAFgycfXhul85xkf8AYs+ZcAT7Mdsi618N6aMSirtQj/PfR/6yRWihgXAHED/dJdqS2cUY6VGnGuyJs1NX7T04SKndJltTf0s0Hz9h0AqekYWYoSynBpnDpp38VXFxUG8VGfLTs+XeYgr6KSSfIB7xCt+GiZSux/l+et1bt89UF1x8qG85vYEtxN4AvQ9vj/SLksMSYQtP4h3/GiiOEO8cHELM52re9OEvk5Zl0G/6T/Iue0xw4L//5vn9JLy6o6QyTQsC+0Bod1fS5Gq3EYF4SLmiDLck5nkhQay8a907GoJbCcg8z0iEsco/mWQ5BVXxyiMv3ZPi1r/wmT+4tBcKBo0pmhuvnUGXqdSNg9QkixLMW238mEuMi7uKi3nGg="] +["send", 871510768697378, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFQeoA5aiNXKMtETGYcmO46T1h42YB8NHr4ekIHwdqZ8rt92b8lIZ2/vJsX/SaXdIZXAm5V95SVY/rIgjg=="] +["receive", 871510769016680, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOi0VAFrBiW7zpzdiH84slfkh2uGmwGj5INszOz8ekypDkR4ReJSXKI95yT9UKvjc+S/uSgWNNO1c6x1o5blV2JsyKPxd0ktshbmPGX1DIh3r0fjwSfHO/Nj0QAx0t363pfAYShDSENGhAII9qvqDkKyTSaf2zZaubm0JuZqCNTWDDSYtxXRVo74jArQ604uxMvab+K4llXiKQ7n95ySfx8eRgjJRAHzXBYl6wi5jnJv/AHC7iv/vId3BZDRicecUm2MjS6RBg6XfOpmY2xzNPl7YwF6HN8wI1QIO8fRbrYOhfzbfEM4MWkGZGd2edm63iuesAO1QZOtYomhTzhK+mA7orCF62GkWAKuj6kZ7XX6Re5pcgeWD6wW8zn0TpTOuikJcqDDR2Qq9nz/P7sI4xBWtadydeZPUnVWPJK3buZOjQ+YeoiyPBDpGUz/s7TSAwI8+swQ8AiFnKMY+TyrUiGgRZ+EMKRPiZqT6Nd9WovvUImxtFZOgEcoYqPeWD0ceoOaEejMeojIXK1dHxqCRRJBRvC4nBqmIFkejTWKyxkf4vgeqFhjaMHi0LRSGSc6uMAAci9K2kx7tnk51RTSOjuHvAhXhGcT1c2H2JbJz9vlVOyVaetpxAn9ePvTsID+qgdgG4rBGau6OqpFidUEnCrdDNSPLuZEAFrsKawZHQ2AncsMrou93Lnx4oEdjw8gzDhHGAF95gCcahS3mqAmLvh5xfczAChXWqRHcbZpKSa1lx8="] +["send", 871510770152451, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFUeoA4xoDLCiedCkANLEtEuGcUfk7bs3HGt8LPWIhOEXacFheZq30bwav224n1OyuWWg6Cfn1E97PjGgA=="] +["receive", 871511536348884, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] +["receive", 871512192149091, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] +["receive", 871512745462688, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] +["receive", 871513688495730, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] +["receive", 871515359269685, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] +["receive", 871520579725115, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOm0VAE4x+ZcR5C+Oxzkj66sGTrE2m1D/wWLvGGxHHg/Tan0R+lDBCjdZCkpaNde5ASGSLcaO6HCqzgv4dw="] +["send", 871520580169533, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFYeoA55LpHDPsHKqvP2S5hDNMtXNPdTmTxcPBzAlsT2XJQEpFpyIbr4VqUL9rnfzO3Wc3uyPGNEsuq1/cfHnw=="] +["receive", 871521781534473, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAANAUwQpfiPFpqjxOPgUwWFgAABUwASBQ0v2kJcKoo0TtVGWhF6P2PUCqhUoCkfXehTfq+VsTsiUCiU4wAyBsgV46uBS95RUxIhVJGj8AOgIh6ntK7a2x6upS7C7d4TAEQQRNB93DQrP0H9vH2Da5pdHl4GrH7JOE86WhpcLA9YjOPGX55SVIktysfVV3cXjUV1nExBDrpcazQnMx6KomBeUANQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] +["receive", 871522417258946, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAANAUwQpfiPFpqjxOPgUwWFgAABUwASBQ0v2kJcKoo0TtVGWhF6P2PUCqhUoCkfXehTfq+VsTsiUCiU4wAyBsgV46uBS95RUxIhVJGj8AOgIh6ntK7a2x6upS7C7d4TAEQQRNB93DQrP0H9vH2Da5pdHl4GrH7JOE86WhpcLA9YjOPGX55SVIktysfVV3cXjUV1nExBDrpcazQnMx6KomBeUANQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] +["receive", 871523033966922, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAANAUwQpfiPFpqjxOPgUwWFgAABUwASBQ0v2kJcKoo0TtVGWhF6P2PUCqhUoCkfXehTfq+VsTsiUCiU4wAyBsgV46uBS95RUxIhVJGj8AOgIh6ntK7a2x6upS7C7d4TAEQQRNB93DQrP0H9vH2Da5pdHl4GrH7JOE86WhpcLA9YjOPGX55SVIktysfVV3cXjUV1nExBDrpcazQnMx6KomBeUANQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]