Get through attestation
This commit is contained in:
parent
391fc0df27
commit
672c8643ea
6 changed files with 334 additions and 59 deletions
|
|
@ -5,11 +5,11 @@ import enum
|
||||||
import hashlib
|
import hashlib
|
||||||
import pathlib
|
import pathlib
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import cryptography
|
import cryptography
|
||||||
|
import ecdsa
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -18,6 +18,18 @@ from . import interaction_model
|
||||||
from . import session
|
from . import session
|
||||||
from . import tlv
|
from . import tlv
|
||||||
|
|
||||||
|
TEST_CERTS = pathlib.Path(
|
||||||
|
"/home/tannewt/repos/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/"
|
||||||
|
)
|
||||||
|
TEST_PAI_CERT_DER = TEST_CERTS / "Chip-Test-PAI-FFF1-8000-Cert.der"
|
||||||
|
TEST_PAI_CERT_PEM = TEST_CERTS / "Chip-Test-PAI-FFF1-8000-Cert.pem"
|
||||||
|
TEST_DAC_CERT_DER = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Cert.der"
|
||||||
|
TEST_DAC_CERT_PEM = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Cert.pem"
|
||||||
|
TEST_DAC_KEY_DER = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Key.der"
|
||||||
|
TEST_DAC_KEY_PEM = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Key.pem"
|
||||||
|
|
||||||
|
TEST_CD_CERT_DER = pathlib.Path("certification_declaration.der")
|
||||||
|
|
||||||
__version__ = "0.0.0"
|
__version__ = "0.0.0"
|
||||||
|
|
||||||
# Section 4.11.2
|
# Section 4.11.2
|
||||||
|
|
@ -192,9 +204,9 @@ class MessageReceptionState:
|
||||||
|
|
||||||
|
|
||||||
class MessageCounter:
|
class MessageCounter:
|
||||||
def __init__(self, starting_value=None):
|
def __init__(self, starting_value=None, random_source=None):
|
||||||
if starting_value is None:
|
if starting_value is None:
|
||||||
starting_value = os.urandom(4)
|
starting_value = random_source.urandom(4)
|
||||||
starting_value = struct.unpack("<I", starting_value)[0]
|
starting_value = struct.unpack("<I", starting_value)[0]
|
||||||
starting_value >>= 4
|
starting_value >>= 4
|
||||||
starting_value += 1
|
starting_value += 1
|
||||||
|
|
@ -305,7 +317,7 @@ class UnsecuredSessionContext:
|
||||||
|
|
||||||
|
|
||||||
class SecureSessionContext:
|
class SecureSessionContext:
|
||||||
def __init__(self, socket, local_session_id):
|
def __init__(self, random_source, socket, local_session_id):
|
||||||
self.session_type = None
|
self.session_type = None
|
||||||
"""Records whether the session was established using CASE or PASE."""
|
"""Records whether the session was established using CASE or PASE."""
|
||||||
self.session_role_initiator = False
|
self.session_role_initiator = False
|
||||||
|
|
@ -320,7 +332,7 @@ class SecureSessionContext:
|
||||||
"""Encrypts data in messages sent from the session establishment responder to the initiator."""
|
"""Encrypts data in messages sent from the session establishment responder to the initiator."""
|
||||||
self.shared_secret = None
|
self.shared_secret = None
|
||||||
"""Computed during the CASE protocol execution and re-used when CASE session resumption is implemented."""
|
"""Computed during the CASE protocol execution and re-used when CASE session resumption is implemented."""
|
||||||
self.local_message_counter = MessageCounter()
|
self.local_message_counter = MessageCounter(random_source=random_source)
|
||||||
"""Secure Session Message Counter for outbound messages."""
|
"""Secure Session Message Counter for outbound messages."""
|
||||||
self.message_reception_state = None
|
self.message_reception_state = None
|
||||||
"""Provides tracking for the Secure Session Message Counter of the remote"""
|
"""Provides tracking for the Secure Session Message Counter of the remote"""
|
||||||
|
|
@ -736,7 +748,7 @@ class StatusReport:
|
||||||
|
|
||||||
|
|
||||||
class SessionManager:
|
class SessionManager:
|
||||||
def __init__(self, socket):
|
def __init__(self, random_source, socket):
|
||||||
persist_path = pathlib.Path("counters.json")
|
persist_path = pathlib.Path("counters.json")
|
||||||
if persist_path.exists():
|
if persist_path.exists():
|
||||||
self.nonvolatile = json.loads(persist_path.read_text())
|
self.nonvolatile = json.loads(persist_path.read_text())
|
||||||
|
|
@ -745,17 +757,22 @@ class SessionManager:
|
||||||
self.nonvolatile["check_in_counter"] = None
|
self.nonvolatile["check_in_counter"] = None
|
||||||
self.nonvolatile["group_encrypted_data_message_counter"] = None
|
self.nonvolatile["group_encrypted_data_message_counter"] = None
|
||||||
self.nonvolatile["group_encrypted_control_message_counter"] = None
|
self.nonvolatile["group_encrypted_control_message_counter"] = None
|
||||||
self.unencrypted_message_counter = MessageCounter()
|
self.unencrypted_message_counter = MessageCounter(random_source=random_source)
|
||||||
self.group_encrypted_data_message_counter = MessageCounter(
|
self.group_encrypted_data_message_counter = MessageCounter(
|
||||||
self.nonvolatile["group_encrypted_data_message_counter"]
|
self.nonvolatile["group_encrypted_data_message_counter"],
|
||||||
|
random_source=random_source,
|
||||||
)
|
)
|
||||||
self.group_encrypted_control_message_counter = MessageCounter(
|
self.group_encrypted_control_message_counter = MessageCounter(
|
||||||
self.nonvolatile["group_encrypted_control_message_counter"]
|
self.nonvolatile["group_encrypted_control_message_counter"],
|
||||||
|
random_source=random_source,
|
||||||
|
)
|
||||||
|
self.check_in_counter = MessageCounter(
|
||||||
|
self.nonvolatile["check_in_counter"], random_source=random_source
|
||||||
)
|
)
|
||||||
self.check_in_counter = MessageCounter(self.nonvolatile["check_in_counter"])
|
|
||||||
self.unsecured_session_context = {}
|
self.unsecured_session_context = {}
|
||||||
self.secure_session_contexts = ["reserved"]
|
self.secure_session_contexts = ["reserved"]
|
||||||
self.socket = socket
|
self.socket = socket
|
||||||
|
self.random = random_source
|
||||||
|
|
||||||
def _increment(self, value):
|
def _increment(self, value):
|
||||||
return (value + 1) % 0xFFFFFFFF
|
return (value + 1) % 0xFFFFFFFF
|
||||||
|
|
@ -836,7 +853,7 @@ class SessionManager:
|
||||||
session_id = self.secure_session_contexts.index(None)
|
session_id = self.secure_session_contexts.index(None)
|
||||||
|
|
||||||
self.secure_session_contexts[session_id] = SecureSessionContext(
|
self.secure_session_contexts[session_id] = SecureSessionContext(
|
||||||
self.socket, session_id
|
self.random, self.socket, session_id
|
||||||
)
|
)
|
||||||
return self.secure_session_contexts[session_id]
|
return self.secure_session_contexts[session_id]
|
||||||
|
|
||||||
|
|
@ -887,41 +904,115 @@ class GeneralCommissioningCluster(data_model.GeneralCommissioningCluster):
|
||||||
self.basic_commissioning_info = basic_commissioning_info
|
self.basic_commissioning_info = basic_commissioning_info
|
||||||
|
|
||||||
def arm_fail_safe(
|
def arm_fail_safe(
|
||||||
self, args: data_model.GeneralCommissioningCluster.ArmFailSafe
|
self, session, args: data_model.GeneralCommissioningCluster.ArmFailSafe
|
||||||
) -> data_model.GeneralCommissioningCluster.ArmFailSafeResponse:
|
) -> data_model.GeneralCommissioningCluster.ArmFailSafeResponse:
|
||||||
response = data_model.GeneralCommissioningCluster.ArmFailSafeResponse()
|
response = data_model.GeneralCommissioningCluster.ArmFailSafeResponse()
|
||||||
response.ErrorCode = data_model.CommissioningErrorEnum.OK
|
response.ErrorCode = data_model.CommissioningErrorEnum.OK
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def set_regulatory_config(
|
def set_regulatory_config(
|
||||||
self, args: data_model.GeneralCommissioningCluster.SetRegulatoryConfig
|
self, session, args: data_model.GeneralCommissioningCluster.SetRegulatoryConfig
|
||||||
) -> data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse:
|
) -> data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse:
|
||||||
response = data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse()
|
response = data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse()
|
||||||
response.ErrorCode = data_model.CommissioningErrorEnum.OK
|
response.ErrorCode = data_model.CommissioningErrorEnum.OK
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class AttestationElements(tlv.Structure):
|
||||||
|
certification_declaration = tlv.OctetStringMember(0x01, max_length=400)
|
||||||
|
attestation_nonce = tlv.OctetStringMember(0x02, max_length=32)
|
||||||
|
timestamp = tlv.IntMember(0x03, signed=False, octets=4)
|
||||||
|
firmware_information = tlv.OctetStringMember(0x04, max_length=16, optional=True)
|
||||||
|
"""Used for secure boot. We don't support it."""
|
||||||
|
|
||||||
|
|
||||||
|
class NOCSRElements(tlv.Structure):
|
||||||
|
csr = tlv.OctetStringMember(0x01, max_length=1024)
|
||||||
|
CSRNonce = tlv.OctetStringMember(0x02, max_length=32)
|
||||||
|
# Skip vendor reserved
|
||||||
|
|
||||||
|
|
||||||
class NodeOperationalCredentialsCluster(data_model.NodeOperationalCredentialsCluster):
|
class NodeOperationalCredentialsCluster(data_model.NodeOperationalCredentialsCluster):
|
||||||
|
def __init__(self):
|
||||||
|
self.dac_key = ecdsa.keys.SigningKey.from_der(
|
||||||
|
TEST_DAC_KEY_DER.read_bytes(), hashfunc=hashlib.sha256
|
||||||
|
)
|
||||||
|
|
||||||
|
self.new_key_for_update = False
|
||||||
|
|
||||||
def certificate_chain_request(
|
def certificate_chain_request(
|
||||||
self, args: data_model.NodeOperationalCredentialsCluster.CertificateChainRequest
|
self,
|
||||||
|
session,
|
||||||
|
args: data_model.NodeOperationalCredentialsCluster.CertificateChainRequest,
|
||||||
) -> data_model.NodeOperationalCredentialsCluster.CertificateChainResponse:
|
) -> data_model.NodeOperationalCredentialsCluster.CertificateChainResponse:
|
||||||
response = (
|
response = (
|
||||||
data_model.NodeOperationalCredentialsCluster.CertificateChainResponse()
|
data_model.NodeOperationalCredentialsCluster.CertificateChainResponse()
|
||||||
)
|
)
|
||||||
if args.CertificateType == data_model.CertificateChainTypeEnum.PAI:
|
if args.CertificateType == data_model.CertificateChainTypeEnum.PAI:
|
||||||
print("PAI")
|
print("PAI")
|
||||||
|
response.Certificate = TEST_PAI_CERT_DER.read_bytes()
|
||||||
elif args.CertificateType == data_model.CertificateChainTypeEnum.DAC:
|
elif args.CertificateType == data_model.CertificateChainTypeEnum.DAC:
|
||||||
print("DAC")
|
print("DAC")
|
||||||
response.Certificate = b""
|
response.Certificate = TEST_DAC_CERT_DER.read_bytes()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def attestation_request(
|
def attestation_request(
|
||||||
self, args: data_model.NodeOperationalCredentialsCluster.AttestationRequest
|
self,
|
||||||
|
session,
|
||||||
|
args: data_model.NodeOperationalCredentialsCluster.AttestationRequest,
|
||||||
) -> data_model.NodeOperationalCredentialsCluster.AttestationResponse:
|
) -> data_model.NodeOperationalCredentialsCluster.AttestationResponse:
|
||||||
print("attestation")
|
print("attestation")
|
||||||
|
elements = AttestationElements()
|
||||||
|
elements.certification_declaration = TEST_CD_CERT_DER.read_bytes()
|
||||||
|
elements.attestation_nonce = args.AttestationNonce
|
||||||
|
elements.timestamp = int(time.time())
|
||||||
|
elements = elements.encode()
|
||||||
|
print("elements", len(elements), elements[:3].hex(" "))
|
||||||
|
print(
|
||||||
|
"challeng",
|
||||||
|
len(session.attestation_challenge),
|
||||||
|
session.attestation_challenge[:3].hex(" "),
|
||||||
|
)
|
||||||
|
attestation_tbs = elements.tobytes() + session.attestation_challenge
|
||||||
response = data_model.NodeOperationalCredentialsCluster.AttestationResponse()
|
response = data_model.NodeOperationalCredentialsCluster.AttestationResponse()
|
||||||
response.AttestationElements = b""
|
response.AttestationElements = elements
|
||||||
response.AttestationSignature = b""
|
response.AttestationSignature = self.dac_key.sign_deterministic(
|
||||||
|
attestation_tbs,
|
||||||
|
hashfunc=hashlib.sha256,
|
||||||
|
sigencode=ecdsa.util.sigencode_string,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def csr_request(
|
||||||
|
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
|
||||||
|
|
||||||
|
self.new_key_for_update = args.IsForUpdateNOC
|
||||||
|
|
||||||
|
# class CSRRequest(tlv.Structure):
|
||||||
|
# CSRNonce = tlv.OctetStringMember(0, 32)
|
||||||
|
# IsForUpdateNOC = tlv.BoolMember(1, optional=True, default=False)
|
||||||
|
|
||||||
|
# Generate a new key pair.
|
||||||
|
new_key_csr = b"TODO"
|
||||||
|
|
||||||
|
# Create a CSR to reply back with. Sign it with the new private key.
|
||||||
|
elements = NOCSRElements()
|
||||||
|
elements.csr = new_key_csr
|
||||||
|
elements.CSRNonce = args.CSRNonce
|
||||||
|
elements = elements.encode()
|
||||||
|
nocsr_tbs = elements.tobytes() + session.attestation_challenge
|
||||||
|
|
||||||
|
# class CSRResponse(tlv.Structure):
|
||||||
|
# NOCSRElements = tlv.OctetStringMember(0, RESP_MAX)
|
||||||
|
# AttestationSignature = tlv.OctetStringMember(1, 64)
|
||||||
|
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
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -933,7 +1024,7 @@ class CircuitMatter:
|
||||||
random_source,
|
random_source,
|
||||||
state_filename,
|
state_filename,
|
||||||
vendor_id=0xFFF1,
|
vendor_id=0xFFF1,
|
||||||
product_id=0,
|
product_id=0x8000,
|
||||||
):
|
):
|
||||||
self.socketpool = socketpool
|
self.socketpool = socketpool
|
||||||
self.mdns_server = mdns_server
|
self.mdns_server = mdns_server
|
||||||
|
|
@ -963,7 +1054,7 @@ class CircuitMatter:
|
||||||
self.socket.bind((UDP_IP, self.UDP_PORT))
|
self.socket.bind((UDP_IP, self.UDP_PORT))
|
||||||
self.socket.setblocking(False)
|
self.socket.setblocking(False)
|
||||||
|
|
||||||
self.manager = SessionManager(self.socket)
|
self.manager = SessionManager(self.random, self.socket)
|
||||||
|
|
||||||
print(f"Listening on UDP port {self.UDP_PORT}")
|
print(f"Listening on UDP port {self.UDP_PORT}")
|
||||||
|
|
||||||
|
|
@ -1039,10 +1130,10 @@ class CircuitMatter:
|
||||||
# report.AttributeStatus = astatus
|
# report.AttributeStatus = astatus
|
||||||
return report
|
return report
|
||||||
|
|
||||||
def invoke(self, cluster, path, fields, command_ref):
|
def invoke(self, session, cluster, path, fields, command_ref):
|
||||||
print("invoke", path)
|
print("invoke", path)
|
||||||
response = interaction_model.InvokeResponseIB()
|
response = interaction_model.InvokeResponseIB()
|
||||||
cdata = cluster.invoke(path, fields)
|
cdata = cluster.invoke(session, path, fields)
|
||||||
if cdata is None:
|
if cdata is None:
|
||||||
cstatus = interaction_model.CommandStatusIB()
|
cstatus = interaction_model.CommandStatusIB()
|
||||||
cstatus.CommandPath = path
|
cstatus.CommandPath = path
|
||||||
|
|
@ -1216,6 +1307,9 @@ class CircuitMatter:
|
||||||
elif protocol_opcode == SecureProtocolOpcode.ICD_CHECK_IN:
|
elif protocol_opcode == SecureProtocolOpcode.ICD_CHECK_IN:
|
||||||
print("Received ICD Check-in")
|
print("Received ICD Check-in")
|
||||||
elif message.protocol_id == ProtocolId.INTERACTION_MODEL:
|
elif message.protocol_id == ProtocolId.INTERACTION_MODEL:
|
||||||
|
secure_session_context = self.manager.secure_session_contexts[
|
||||||
|
message.session_id
|
||||||
|
]
|
||||||
if protocol_opcode == InteractionModelOpcode.READ_REQUEST:
|
if protocol_opcode == InteractionModelOpcode.READ_REQUEST:
|
||||||
print("Received Read Request")
|
print("Received Read Request")
|
||||||
read_request, _ = interaction_model.ReadRequestMessage.decode(
|
read_request, _ = interaction_model.ReadRequestMessage.decode(
|
||||||
|
|
@ -1272,7 +1366,12 @@ class CircuitMatter:
|
||||||
cluster = self._endpoints[endpoint][path.Cluster]
|
cluster = self._endpoints[endpoint][path.Cluster]
|
||||||
path.Endpoint = endpoint
|
path.Endpoint = endpoint
|
||||||
invoke_responses.append(
|
invoke_responses.append(
|
||||||
self.invoke(cluster, path, invoke.CommandFields)
|
self.invoke(
|
||||||
|
secure_session_context,
|
||||||
|
cluster,
|
||||||
|
path,
|
||||||
|
invoke.CommandFields,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Cluster 0x{path.Cluster:02x} not found")
|
print(f"Cluster 0x{path.Cluster:02x} not found")
|
||||||
|
|
@ -1281,6 +1380,7 @@ class CircuitMatter:
|
||||||
cluster = self._endpoints[path.Endpoint][path.Cluster]
|
cluster = self._endpoints[path.Endpoint][path.Cluster]
|
||||||
invoke_responses.append(
|
invoke_responses.append(
|
||||||
self.invoke(
|
self.invoke(
|
||||||
|
secure_session_context,
|
||||||
cluster,
|
cluster,
|
||||||
path,
|
path,
|
||||||
invoke.CommandFields,
|
invoke.CommandFields,
|
||||||
|
|
|
||||||
128
circuitmatter/certificates.py
Normal file
128
circuitmatter/certificates.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
# This file should only be needed when generating certificates.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from . import tlv
|
||||||
|
from .data_model import Enum8
|
||||||
|
|
||||||
|
import ecdsa
|
||||||
|
from ecdsa import der
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
class CertificationType(Enum8):
|
||||||
|
DEVELOPMENT_AND_TEST = 0
|
||||||
|
PROVISIONAL = 1
|
||||||
|
OFFICIAL = 2
|
||||||
|
|
||||||
|
|
||||||
|
class CertificationDeclaration(tlv.Structure):
|
||||||
|
format_version = tlv.IntMember(0, signed=False, octets=1)
|
||||||
|
vendor_id = tlv.IntMember(1, signed=False, octets=2)
|
||||||
|
product_id_array = tlv.ArrayMember(
|
||||||
|
2, tlv.IntMember(0, signed=False, octets=2), max_length=100
|
||||||
|
)
|
||||||
|
device_type_id = tlv.IntMember(3, signed=False, octets=4)
|
||||||
|
certificate_id = tlv.UTF8StringMember(4, max_length=19)
|
||||||
|
security_level = tlv.IntMember(5, signed=False, octets=1)
|
||||||
|
security_information = tlv.IntMember(6, signed=False, octets=2)
|
||||||
|
version_number = tlv.IntMember(7, signed=False, octets=2)
|
||||||
|
certification_type = tlv.EnumMember(8, CertificationType)
|
||||||
|
dac_origin_vendor_id = tlv.IntMember(9, signed=False, octets=2, optional=True)
|
||||||
|
dac_origin_product_id = tlv.IntMember(10, signed=False, octets=2, optional=True)
|
||||||
|
authorized_paa_list = tlv.ArrayMember(
|
||||||
|
11, tlv.OctetStringMember(None, max_length=20), optional=True, max_length=10
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 generate_certificates(
|
||||||
|
vendor_id=0xFFF1, product_id=0x8000, device_type=22, prefix=None
|
||||||
|
):
|
||||||
|
declaration = CertificationDeclaration()
|
||||||
|
declaration.format_version = 1 # Always 1
|
||||||
|
declaration.vendor_id = vendor_id
|
||||||
|
declaration.product_id_array = [product_id]
|
||||||
|
declaration.device_type_id = 0x1234 # device_type
|
||||||
|
declaration.certificate_id = "ZIG20141ZB330001-24" # "CSA00000SWC00000-00"
|
||||||
|
declaration.security_level = 0 # Always 0
|
||||||
|
declaration.security_information = 0 # Always 0
|
||||||
|
declaration.version_number = 0x2694 # 1 # Always 1
|
||||||
|
declaration.certification_type = CertificationType.DEVELOPMENT_AND_TEST
|
||||||
|
declaration = declaration.encode()
|
||||||
|
|
||||||
|
for i in range(0, len(declaration), 16):
|
||||||
|
print(f"{i:08x}", declaration[i : i + 16].hex(" "))
|
||||||
|
|
||||||
|
# From: https://github.com/project-chip/matter.js/blob/main/packages/protocol/src/certificate/CertificationDeclarationManager.ts
|
||||||
|
# NIST256p is the same as secp256r1
|
||||||
|
private_key = ecdsa.keys.SigningKey.from_string(
|
||||||
|
b"\xae\xf3\x48\x41\x16\xe9\x48\x1e\xc5\x7b\xe0\x47\x2d\xf4\x1b\xf4\x99\x06\x4e\x50\x24\xad\x86\x9e\xca\x5e\x88\x98\x02\xd4\x80\x75",
|
||||||
|
curve=ecdsa.curves.NIST256p,
|
||||||
|
hashfunc=hashlib.sha256,
|
||||||
|
)
|
||||||
|
print(private_key.to_string().hex().upper())
|
||||||
|
subject_key_identifier = b"\x62\xfa\x82\x33\x59\xac\xfa\xa9\x96\x3e\x1c\xfa\x14\x0a\xdd\xf5\x04\xf3\x71\x60"
|
||||||
|
signature = private_key.sign_deterministic(
|
||||||
|
declaration,
|
||||||
|
hashfunc=hashlib.sha256,
|
||||||
|
sigencode=ecdsa.util.sigencode_der_canonize,
|
||||||
|
)
|
||||||
|
print("signature", signature.hex(" "))
|
||||||
|
|
||||||
|
certification_declaration = []
|
||||||
|
# version
|
||||||
|
certification_declaration.append(der.encode_integer(3))
|
||||||
|
# Digest algorithm
|
||||||
|
certification_declaration.append(
|
||||||
|
encode_set(der.encode_sequence(der.encode_oid(2, 16, 840, 1, 101, 3, 4, 2, 1)))
|
||||||
|
)
|
||||||
|
# encap content info
|
||||||
|
encap_content_info = []
|
||||||
|
# content type
|
||||||
|
encap_content_info.append(der.encode_oid(1, 2, 840, 113549, 1, 7, 1))
|
||||||
|
# content
|
||||||
|
encap_content_info.append(
|
||||||
|
der.encode_constructed(0, der.encode_octet_string(declaration))
|
||||||
|
)
|
||||||
|
certification_declaration.append(der.encode_sequence(*encap_content_info))
|
||||||
|
|
||||||
|
signer_info = []
|
||||||
|
# version
|
||||||
|
signer_info.append(der.encode_integer(3))
|
||||||
|
# subject key identifier
|
||||||
|
signer_info.append(
|
||||||
|
b"\x80"
|
||||||
|
+ der.encode_length(len(subject_key_identifier))
|
||||||
|
+ subject_key_identifier
|
||||||
|
)
|
||||||
|
# digest algorithm
|
||||||
|
signer_info.append(
|
||||||
|
der.encode_sequence(der.encode_oid(2, 16, 840, 1, 101, 3, 4, 2, 1))
|
||||||
|
)
|
||||||
|
# signature algorithm
|
||||||
|
signer_info.append(der.encode_sequence(der.encode_oid(1, 2, 840, 10045, 4, 3, 2)))
|
||||||
|
# signature
|
||||||
|
signer_info.append(der.encode_octet_string(signature))
|
||||||
|
certification_declaration.append(encode_set(der.encode_sequence(*signer_info)))
|
||||||
|
|
||||||
|
signed_data = []
|
||||||
|
signed_data.append(der.encode_oid(1, 2, 840, 113549, 1, 7, 2))
|
||||||
|
cd = der.encode_sequence(*certification_declaration)
|
||||||
|
signed_data.append(der.encode_constructed(0, cd))
|
||||||
|
cms_signed = der.encode_sequence(*signed_data)
|
||||||
|
|
||||||
|
return cms_signed
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cd = generate_certificates()
|
||||||
|
pathlib.Path("certification_declaration.der").write_bytes(cd)
|
||||||
|
for i in range(0, len(cd), 16):
|
||||||
|
print(f"{i:08x}", cd[i : i + 16].hex(" "))
|
||||||
|
print(cd.hex(" "))
|
||||||
|
|
@ -172,7 +172,9 @@ class Cluster:
|
||||||
if not field_name.startswith("_") and isinstance(descriptor, Command):
|
if not field_name.startswith("_") and isinstance(descriptor, Command):
|
||||||
yield field_name, descriptor
|
yield field_name, descriptor
|
||||||
|
|
||||||
def invoke(self, path, fields) -> Optional[interaction_model.CommandDataIB]:
|
def invoke(
|
||||||
|
self, session, path, fields
|
||||||
|
) -> Optional[interaction_model.CommandDataIB]:
|
||||||
found = False
|
found = False
|
||||||
for field_name, descriptor in self._commands():
|
for field_name, descriptor in self._commands():
|
||||||
if descriptor.command_id != path.Command:
|
if descriptor.command_id != path.Command:
|
||||||
|
|
@ -183,7 +185,7 @@ class Cluster:
|
||||||
print(arg)
|
print(arg)
|
||||||
command = getattr(self, field_name)
|
command = getattr(self, field_name)
|
||||||
if callable(command):
|
if callable(command):
|
||||||
result = command(arg)
|
result = command(session, arg)
|
||||||
else:
|
else:
|
||||||
print(field_name, "not implemented")
|
print(field_name, "not implemented")
|
||||||
return None
|
return None
|
||||||
|
|
@ -458,7 +460,7 @@ class NodeOperationalCredentialsCluster(Cluster):
|
||||||
IsForUpdateNOC = tlv.BoolMember(1, optional=True, default=False)
|
IsForUpdateNOC = tlv.BoolMember(1, optional=True, default=False)
|
||||||
|
|
||||||
class CSRResponse(tlv.Structure):
|
class CSRResponse(tlv.Structure):
|
||||||
CSR = tlv.OctetStringMember(0, RESP_MAX)
|
NOCSRElements = tlv.OctetStringMember(0, RESP_MAX)
|
||||||
AttestationSignature = tlv.OctetStringMember(1, 64)
|
AttestationSignature = tlv.OctetStringMember(1, 64)
|
||||||
|
|
||||||
class AddNOC(tlv.Structure):
|
class AddNOC(tlv.Structure):
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,8 @@ def compute_session_keys(Ke, secure_session_context):
|
||||||
tag_length=session.CRYPTO_AEAD_MIC_LENGTH_BYTES,
|
tag_length=session.CRYPTO_AEAD_MIC_LENGTH_BYTES,
|
||||||
)
|
)
|
||||||
secure_session_context.attestation_challenge = keys[
|
secure_session_context.attestation_challenge = keys[
|
||||||
2 * session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES :
|
2 * session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES : 3
|
||||||
|
* session.CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -434,11 +434,43 @@ class NumberMember(Member[_NT, _OPT, _NULLABLE], Generic[_NT, _OPT, _NULLABLE]):
|
||||||
return f"{value}{unsigned}"
|
return f"{value}{unsigned}"
|
||||||
|
|
||||||
def encode_element_type(self, value):
|
def encode_element_type(self, value):
|
||||||
# We don't adjust our encoding based on value size. We always use the bytes needed for the
|
if self.integer:
|
||||||
# format.
|
bit_length = value.bit_length()
|
||||||
|
if self.signed:
|
||||||
|
type = ElementType.SIGNED_INT
|
||||||
|
else:
|
||||||
|
type = ElementType.UNSIGNED_INT
|
||||||
|
length = 0 # in power of two
|
||||||
|
if bit_length <= 8:
|
||||||
|
length = 0
|
||||||
|
elif bit_length <= 16:
|
||||||
|
length = 1
|
||||||
|
elif bit_length <= 32:
|
||||||
|
length = 2
|
||||||
|
else:
|
||||||
|
length = 3
|
||||||
|
return type | length
|
||||||
return self._element_type
|
return self._element_type
|
||||||
|
|
||||||
def encode_value_into(self, value, buffer, offset) -> int:
|
def encode_value_into(self, value, buffer, offset) -> int:
|
||||||
|
if self.integer:
|
||||||
|
bit_length = value.bit_length()
|
||||||
|
format_string = None
|
||||||
|
if bit_length <= 8:
|
||||||
|
format_string = "<b" if self.signed else "<B"
|
||||||
|
length = 1
|
||||||
|
elif bit_length <= 16:
|
||||||
|
format_string = "<h" if self.signed else "<H"
|
||||||
|
length = 2
|
||||||
|
elif bit_length <= 32:
|
||||||
|
format_string = "<i" if self.signed else "<I"
|
||||||
|
length = 4
|
||||||
|
else:
|
||||||
|
format_string = "<q" if self.signed else "<Q"
|
||||||
|
length = 8
|
||||||
|
struct.pack_into(format_string, buffer, offset, value)
|
||||||
|
return offset + length
|
||||||
|
# Float
|
||||||
struct.pack_into(self.format, buffer, offset, value)
|
struct.pack_into(self.format, buffer, offset, value)
|
||||||
return offset + self.max_value_length
|
return offset + self.max_value_length
|
||||||
|
|
||||||
|
|
@ -640,14 +672,16 @@ class ArrayMember(Member[_TLVStruct, _OPT, _NULLABLE]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tag,
|
tag,
|
||||||
substruct_class: Type[_TLVStruct],
|
substruct_class: Type[_TLVStruct, Member],
|
||||||
*,
|
*,
|
||||||
|
max_length: Optional[int] = None,
|
||||||
optional: _OPT = False,
|
optional: _OPT = False,
|
||||||
nullable: _NULLABLE = False,
|
nullable: _NULLABLE = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.substruct_class = substruct_class
|
self.substruct_class = substruct_class
|
||||||
self.max_value_length = 1280
|
self.max_value_length = 1280
|
||||||
|
self.max_items = max_length
|
||||||
super().__init__(tag, optional=optional, nullable=nullable, **kwargs)
|
super().__init__(tag, optional=optional, nullable=nullable, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -677,8 +711,11 @@ class ArrayMember(Member[_TLVStruct, _OPT, _NULLABLE]):
|
||||||
buffer[offset] = ElementType.STRUCTURE
|
buffer[offset] = ElementType.STRUCTURE
|
||||||
elif isinstance(v, List):
|
elif isinstance(v, List):
|
||||||
buffer[offset] = ElementType.LIST
|
buffer[offset] = ElementType.LIST
|
||||||
else:
|
elif isinstance(self.substruct_class, Member):
|
||||||
raise NotImplementedError("Unknown type")
|
buffer[offset] = self.substruct_class.encode_element_type(v)
|
||||||
|
print(offset, hex(buffer[offset]))
|
||||||
|
offset = self.substruct_class.encode_value_into(v, buffer, offset + 1)
|
||||||
|
continue
|
||||||
offset = v.encode_into(buffer, offset + 1)
|
offset = v.encode_into(buffer, offset + 1)
|
||||||
buffer[offset] = ElementType.END_OF_CONTAINER
|
buffer[offset] = ElementType.END_OF_CONTAINER
|
||||||
return offset + 1
|
return offset + 1
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,34 @@
|
||||||
["urandom", 683495367840819, 8, "TVCaLwZCw7U="]
|
["urandom", 784666670915434, 4, "gTyCfg=="]
|
||||||
["receive", 683498821765516, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "BAAAAF8dUQHg9sDgbF2ERwUgjxgAABUwASASKWbFjMO2TX2TttCy5g/kYva9Xkq3bUgRNk2ZJLC+MCUCVLskAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="]
|
["urandom", 784666670932045, 4, "WEtNXg=="]
|
||||||
["urandom", 683498851412134, 32, "MXZ+qUCRDQzGtLd/BdEFclIVyeARZatikL292J53wec="]
|
["urandom", 784666670944279, 4, "Im0D9Q=="]
|
||||||
["send", 683498851505099, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AQAAAJ0RXQ7g9sDgbF2ERwIhjxgAAF8dUQEVMAEgEilmxYzDtk19k7bQsuYP5GL2vV5Kt21IETZNmSSwvjAwAiAxdn6pQJENDMa0t38F0QVyUhXJ4BFlq2KQvb3YnnfB5yUDAQA1BCYBECcAADACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="]
|
["urandom", 784666670954137, 4, "q3+7MQ=="]
|
||||||
["receive", 683498857474653, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "BAAAAGAdUQHg9sDgbF2ERwUijxgAABUwAUEE20SFW+IKRC2BOynyHg8nHPTv3+LVnM+u4ETTCvbrvnTgkJQNNj/qtVQNZWLUPqJlGYTqUqMqQtXrIkE5M4Ih6Bg="]
|
["urandom", 784666670967192, 8, "GdHmWrzZGms="]
|
||||||
["randbelow", 683498857567458, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 55163913805809046006520747928954323885679067894514874675221401915371371566658]
|
["receive", 784671745464646, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "BAAAANR1RwbAzDLm0ZLOIQUgmg0AABUwASD/PukDdOXeqJ1IBdpHgqsgOMm3wxKzsceqrEXbVN2XnyUCvzMkAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="]
|
||||||
["send", 683498872841814, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AQAAAJ4RXQ7g9sDgbF2ERwIjjxgAAGAdUQEVMAFBBAbpQBc797XYmoP1v/VvwWAkH1y/0bDLthCBm0x0+NeykQt9xIvt80/Bs/ho3rZp+4j+S19V7CvxDJBpB47VKPowAiBwLzDgwDToYO6xBjEMzXZ7MVUWXPf90Xa9lWnufbGeaxg="]
|
["urandom", 784671771475666, 32, "R9etlJW4WJ2hI+MXNURAUgqg0EAvyRTyMrcduh1jOSE="]
|
||||||
["receive", 683498873254503, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "BAAAAGEdUQHg9sDgbF2ERwUkjxgAABUwASB8b9Xw7GZRd3GHmsZJ3Nv29JDKXZbinVKGP2m8Qt61Xxg="]
|
["urandom", 784671771496185, 4, "SqSOEg=="]
|
||||||
["send", 683498873329966, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AQAAAJ8RXQ7g9sDgbF2ERwJAjxgAAGEdUQEAAAAAAAAAAA=="]
|
["send", 784671771593809, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AQAAAMoj6AfAzDLm0ZLOIQIhmg0AANR1RwYVMAEg/z7pA3Tl3qidSAXaR4KrIDjJt8MSs7HHqqxF21Tdl58wAiBH162UlbhYnaEj4xc1REBSCqDQQC/JFPIytx26HWM5ISQDATUEJQEQJzACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="]
|
||||||
["receive", 683498873509204, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADB0XwokT96ZD3xGZKllpiUod7YhF1weUtaXchVJGbNRBI7DUPwBeY2uJgsA7Bor4qb+XsCPzxzdenS8hxQax5z7FCBOY6a5SSQi4mryj30rTt0SjrN6KdIknnTYQks8GXeMaz7VlOrdF2CplNRXF+AGxS1CXxiqr/kuhAw4WFE="]
|
["receive", 784671777601546, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "BAAAANV1RwbAzDLm0ZLOIQUimg0AABUwAUEEEqRKQ9IIhbFzmFRT5DujfzWbge9+63W++o8ir/oUfxBKkLElBbBaT+n96w7pMk6pxi/JfmJwouj3RjZYy4O4MRg="]
|
||||||
["send", 683498874159352, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AME8eQ7ekgBSkCMyEut7Gwn6MYQenzBIWdNgAio6brAJTddb2DqSjWPUS1NQ2Nh6wwipFd1KRLH/F3ruQ5/+dSXcoPlV4J8T0Jm8eqx0o6DayguwFAyu1N5yiZk5bl5GmxYlEFlUi0VPOMcPPzYnqNbsYIgOAO63XcfBHNJsA5vCNsaKnqAktWHtzXmXtZTvZgEHJGJD8LIhZ6lM8wd1Ui3zFlB4GCG7dWjgn13z+TSbgHHHyt1dnj7a/uNxMwrWrQgSnkL/tzrBL0xSwAiVuwtBZpGdab85icqUkQeL6OZdYk21Oz9MVjHuGPrXq3npNX76/wgP3+rAeHc+r1Pt/qEd+KHbG4G+OHUTlIHygGth57dUDM5rum5AdW3/8YTx+tAncedBybrAlikzDh+SYpztfAQG"]
|
["randbelow", 784671777703077, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 26649162366983277512998315276067044341880723378171385531045348526206345387847]
|
||||||
["receive", 683498875161114, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADF0XwrZIgz9knLzOrJkDSf9kwXRYOEpfY5UfzmZM7/qdko="]
|
["send", 784671787820468, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AQAAAMsj6AfAzDLm0ZLOIQIjmg0AANV1RwYVMAFBBMSdK7qIGwnaU3L8sov92kGKTqOhK56kBKAj55TYu3mZ9mLXCQrXPeZ/38c/YMxeIJ3EAfN/bOR6ec35cqwV0yAwAiAiuC1nE3Ubp++vlT/OYtqaqGoFdt/Usottde5xkgwMShg="]
|
||||||
["receive", 683498875255833, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADJ0Xwr/eCKktmaP1Uc1+Zp5aNJM9lEmVebne+lDI+DNiP2XImSTH8ARBUcbF9HLDK1/4dT55AGOts4iZ0ZNOZFVXAru45pVapgT8bN8uxPYgQ=="]
|
["receive", 784671788261229, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "BAAAANZ1RwbAzDLm0ZLOIQUkmg0AABUwASCsVcEv3Eh8OPfsmxLISxCVu7qwCbMN7+LL3jXTvSIkkRg="]
|
||||||
["send", 683498875486027, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMI8eQ4P2pPGapfOg39gOtTZgx8gbquT4vchxWodZJvZeTFpbBUvommihSJ/3+D7sTifa9gz+fM9rdyWF1rd4Cx7EyM="]
|
["send", 784671788334538, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AQAAAMwj6AfAzDLm0ZLOIQJAmg0AANZ1RwYAAAAAAAAAAA=="]
|
||||||
["receive", 683498875778780, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADN0XwoUf68e6LmehobmSAeEE7+bJ9hopGfmhjlTakDo8ug="]
|
["receive", 784671788501062, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJPvywjQvaLnorG7I/AQoh4IJIg9SEd3owBKpVZd2KNZwHF5nlhQ8BhHzr/wpTFRUXtHo0kznWwiReoKTEnjb2e0lKSaGoAP5UADc8o+/IaJl5rvUneuijc9G9HIpcH4faBYMPQiOustDPLjUyDGobn/WYmPs29V1fle1Mizu5Y="]
|
||||||
["receive", 683498875892114, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADR0XwrxT37Olu9HyEQPuSGLVT3EjolLh8dB3hIb4YO/Q/vxakfp1Qs7D8fPhLUjI1jjph5HfD2ogeuQe1M="]
|
["send", 784671789179181, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEbqKAGkyTFFEYXsJR7xYOlDV6nG3ES3MXEEYJHxjkfLv/u19GYfxnFvWDylvm+NbKnprXNebZiVdveov7t1fuaYovmxjv+nZ0RfQBkrabKLERGg5VKxeVoQxFYuIhqRj4iaXJAC3PQjmFA9Lrw6t3VNgcmG1yHa1fY5210krbhRPkxSPBFkbFxRDz+ige2uvRTzdANY1tI5ZhfiYYfC3LwaJsuuSuxhwIXbFoE/5qu8Zp/nAKGZme5PtrBhZcJDMOjKcsfK1Qgyn4kDTUiUMBSIBB2QGuoUf0db6R44fbcNlVvShYI="]
|
||||||
["send", 683498876146515, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMM8eQ7tgujEMBfFaIlrN/xCl8Y4a2JH5UqEYxuRQeJHjMpRBm6WTBFnXXA5KuInASn40rPnidJjVMgnXJvaUeNbqwsM2tIu"]
|
["receive", 784671790132390, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJTvywgs9CHUeFUMwzTra9wAscaZOCzTg6O62BGTYcVAuHo="]
|
||||||
["receive", 683498876473603, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADV0XwqZojBtHTLIdbFe56V1UIE4WaooRa60ax+r6P31Jhxj1n075g/a3tNUOEfyBkmQJSgUH0MQxSz84D1YFtaZAw=="]
|
["receive", 784671790226888, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJXvywiLBDIrxesC/ZX4yeRwt9gndvFwbE8ip2lWnOCxh1u0yv3FKPUVW6+0Ks1uqo6aOJ9FLvGHeyidHLhA2wL1yXSikgvl6s8PDVoniSPXSg=="]
|
||||||
["send", 683498876708616, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMQ8eQ64jIF0YadOHTE9nqad5MkK1by0uhBSadiviVXlc03a1WLgBkySoX6214T1XIwpMTxWrZfVaV17z+S3jL2okeSrmdDE"]
|
["send", 784671790449899, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEfqKAGsoFJIDBcl+c0GSsGQHaX5ZSkQLeprcL862VIIkH7a35ywtoJCtDFEOEJISHst2s0HB8NBkg=="]
|
||||||
["receive", 683498877025054, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADZ0Xwo9WH3ur9aGpocl4AKUtZkAFiAK87+d4cQqMjAHsUt5kTWAtvekb1R0w+/kYsC5QwVklDk4lvQ="]
|
["receive", 784671794339509, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJbvywiVXApjl2O2rCpEp6tCMdZsxXJ8Fg9hz1I3wbfwk3Y="]
|
||||||
["send", 683498877240551, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMU8eQ5vFsc6CWDDdoCuIls1wkgctrxuFrnBzZRCtLNK9Qt3HRb1C6vSRNJ0pGU27NuN6l2k9mEHCy0WNhERx8oxUP39"]
|
["receive", 784671794460868, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJfvywhxaDeamkfBt6JM14rzvD6822cZ8/A2oFzI/B4cjRr+rVN4Xo6BJ/4XSWtexshQvDyDgAfufoyfaBs="]
|
||||||
["receive", 683498877583799, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADd0XwrDKFXAh2biGtXh+eG+a37SSK7iHHclvobcxFZJT3TFpKlMw5y75O8h1yrySPNt+RKjWKqx13c="]
|
["send", 784671794721990, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEjqKAEuoNxQBRLD1VXDDfxtRJk9II/gIrTGAtPcUPEC4bSY+8oIfHmiCkjjl3r9E3ESLv8LaAMp4ncUqorFrA=="]
|
||||||
["send", 683498877788546, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMY8eQ6Is+Atiw2ABb7FL5iy1L6mM5g/k7414s1u2n/lLreAXNCJWDu+dhXcgeK8jXO+nGMOa+PDR8lpj4Lwrxi0MP/h"]
|
["receive", 784671795072291, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJjvywjZRYWm/kdLVbc/vtoKHymQDD1edYM1hHd3rU60iykpJB/Mr2hDRYYc9/zgJcqL/DlpqUaLEpXrJlEYN4teYA=="]
|
||||||
["receive", 683498878095526, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADh0Xwq1tLR8JSXMAh/4eBGVh9nvr6c7D5xraOqx3CTV4Usdv049V7JfXqJVAppEJvHMOXk+ZCUyObv60Q2+lgvaj+lGhA6W/b+5ztZVo8OJIcZ9Rll1FgrwTA=="]
|
["send", 784671795307716, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AL8zAEnqKAHlAQA5+esg7SoD5Jcsx413iVa2qW5mPR765RLosKM15P7MhfPU6VISFU4N6Wi7ejm4wQp2bIlXPItFdg=="]
|
||||||
["send", 683498878325730, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMc8eQ4DBvpgfxkI8u77TH5UJBeFjkKJgW4W1SxT6Gfp2ZKvNNJOjEDmt/G/vWgzZlvcUq9jKCqQdTkn2MlT0aSwItbPLjJ1"]
|
["receive", 784671795554070, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 34041, 0, 0], "AAEAAJnvywhA/bnhfPrxP7AHJa6AbuGIQ2/rp1frP/aWI5lqQp2y33YL++YHMZPV9cZ/jaPoFEXltq1LaAw="]
|
||||||
["receive", 683498878665963, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADl0Xwoh7zyfoLawEVHK/jEfWmf+jEPDbe5cme3DeR/UBcQz1OhT4f8oHNQk6nPIt+Ss0mNHWS7Fapss/z0="]
|
["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"]
|
||||||
["send", 683498878871692, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMg8eQ5q8qqBOCg+y9xQcPIrn3pfJVHbKBSkMMzzOtyYKIMD/ZK4O0TLX8EqzIdOtmdET05jajUN5yWSG6rWZqQcaKiygGUc"]
|
["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=="]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue