lib/libesp32/berry_matter/MATTER_SESSION_ARCHITECTURE.md
This document describes the session establishment architecture in Tasmota's Matter implementation. The implementation supports two types of session establishment:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Matter_Device │
│ (Main device controller - Matter_zz_Device.be) │
│ - Orchestrates all Matter operations │
│ - Manages plugins, UDP server, message handler │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ owns
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Matter_Session_Store │
│ (Session persistence - Matter_Session_Store.be) │
│ - Manages all sessions and fabrics │
│ - Persists to /_matter_fabrics.json │
│ - Creates/removes sessions and fabrics │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ contains
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Matter_Fabric │
│ (Fabric management - Matter_Fabric.be) │
│ - Represents a commissioned fabric (controller ecosystem) │
│ - Stores NOC, ICAC, root CA, IPK │
│ - Contains list of CASE sessions │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ contains
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Matter_Session │
│ (Session state - Matter_Session.be) │
│ - Represents a single secure session │
│ - Stores encryption keys (i2r, r2i) │
│ - Manages message counters │
│ - Tracks session mode (PASE/CASE) │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ UDP ┌──────────────────┐
│ Matter │◄────────────►│ Matter_UDP │
│ Controller │ │ Server │
└──────────────┘ └────────┬─────────┘
│
│ raw bytes
▼
┌──────────────────┐
│ Matter_Message │
│ Handler │
│ (dispatch) │
└────────┬─────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Commissioning │ │ Interaction │ │ Control │
│ Context │ │ Model (IM) │ │ Message │
│ (PASE/CASE) │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
The main orchestrator class that:
class Matter_Device
var sessions # Matter_Session_Store
var message_handler # Matter_MessageHandler
var commissioning # Matter_Commissioning
var udp_server # Matter_UDPServer
var plugins # List of endpoint plugins
Manages all sessions and fabrics:
/_matter_fabrics.jsonclass Matter_Session_Store
var sessions # matter.Expirable_list of sessions
var fabrics # matter.Expirable_list of fabrics
def create_session(local_session_id, initiator_session_id)
def get_session_by_local_session_id(id)
def find_session_by_resumption_id(resumption_id)
def add_fabric(fabric)
def remove_fabric(fabric)
def save_fabrics()
def load_fabrics()
Represents a commissioned fabric:
class Matter_Fabric : Matter_Expirable
var fabric_id # bytes(8) - fabric identifier
var device_id # bytes(8) - our device ID in this fabric
var fabric_compressed # compressed fabric ID
var root_ca_certificate # Root CA certificate
var noc # Node Operational Certificate
var icac # Intermediate CA Certificate
var ipk_epoch_key # IPK for group key derivation
var no_private_key # Device's private key
var _sessions # List of CASE sessions
Represents a single secure session:
class Matter_Session : Matter_Expirable
static var _PASE = 1 # PASE mode
static var _CASE = 2 # CASE mode
var mode # Current session mode
var local_session_id # Our session ID
var initiator_session_id # Controller's session ID
var i2rkey # Initiator to Responder key
var r2ikey # Responder to Initiator key
var attestation_challenge # For attestation
var counter_rcv # Receive counter
var counter_snd # Send counter
var resumption_id # For CASE resumption
var shared_secret # ECDH shared secret
var _fabric # Reference to fabric
Central message dispatcher:
class Matter_MessageHandler
var commissioning # Matter_Commisioning_Context
var im # Matter_IM (Interaction Model)
var control_message # Matter_Control_Message
def msg_received(raw, addr, port)
def send_response_frame(msg)
Handles PASE and CASE protocol exchanges:
class Matter_Commisioning_Context
def process_incoming(msg)
def parse_PBKDFParamRequest(msg)
def parse_Pake1(msg)
def parse_Pake3(msg)
def parse_Sigma1(msg)
def parse_Sigma3(msg)
Message encoding/decoding:
class Matter_Frame
var session # Associated session
var raw # Raw message bytes
var local_session_id # Session ID from header
var message_counter # Message counter
var opcode # Protocol opcode
var exchange_id # Exchange identifier
def decode_header()
def decode_payload()
def encode_frame(payload)
def encrypt()
def decrypt()
PASE is used during initial device commissioning when the controller knows the device's passcode.
Controller Device
│ │
│ ──── PBKDFParamRequest (0x20) ────────► │
│ - initiatorRandom │
│ - initiator_session_id │
│ - passcodeId │
│ │
│ ◄──── PBKDFParamResponse (0x21) ─────── │
│ - initiatorRandom (echo) │
│ - responderRandom │
│ - responderSessionId │
│ - pbkdf_parameters (salt, iterations)│
│ │
│ ──── Pake1 (0x22) ─────────────────────► │
│ - pA (SPAKE2+ public value) │
│ │
│ ◄──── Pake2 (0x23) ──────────────────── │
│ - pB (SPAKE2+ public value) │
│ - cB (SPAKE2+ confirmation) │
│ │
│ ──── Pake3 (0x24) ─────────────────────► │
│ - cA (SPAKE2+ confirmation) │
│ │
│ ◄──── StatusReport (0x40) ───────────── │
│ - SUCCESS │
│ │
│ ═══════ Session Established ═══════════ │
PBKDFParamRequest Processing (parse_PBKDFParamRequest):
Pake1 Processing (parse_Pake1):
Pake3 Processing (parse_Pake3):
CASE is used for subsequent connections after commissioning, using certificates.
Controller Device
│ │
│ ──── Sigma1 (0x30) ────────────────────► │
│ - initiatorRandom │
│ - initiator_session_id │
│ - destinationId │
│ - initiatorEphPubKey │
│ - [resumptionID] (optional) │
│ - [initiatorResumeMIC] (optional) │
│ │
│ ◄──── Sigma2 (0x31) ─────────────────── │
│ - responderRandom │
│ - responderSessionId │
│ - responderEphPubKey │
│ - encrypted2 (TBEData2) │
│ │
│ ──── Sigma3 (0x32) ────────────────────► │
│ - TBEData3Encrypted │
│ │
│ ◄──── StatusReport (0x40) ───────────── │
│ - SUCCESS │
│ │
│ ═══════ Session Established ═══════════ │
If the controller has a valid resumption_id from a previous session:
Controller Device
│ │
│ ──── Sigma1 (0x30) ────────────────────► │
│ - initiatorRandom │
│ - resumptionID │
│ - initiatorResumeMIC │
│ │
│ ◄──── Sigma2Resume (0x33) ───────────── │
│ - resumptionID (new) │
│ - sigma2ResumeMIC │
│ - responderSessionID │
│ │
│ ═══════ Session Resumed ═══════════════ │
Sigma1 Processing (parse_Sigma1):
Sigma2 Generation:
Sigma3 Processing (parse_Sigma3):
passcode (4 bytes)
│
▼
┌─────────────────────────────────────────┐
│ PBKDF2-HMAC-SHA256 │
│ - salt: commissioning_salt (16 bytes) │
│ - iterations: 1000 (default) │
│ - output: 80 bytes │
└─────────────────────────────────────────┘
│
├── w0s (40 bytes) ──► w0 = mod(w0s, P256_order)
│
└── w1s (40 bytes) ──► w1 = mod(w1s, P256_order)
│
▼
L = w1 * G (public key)
ECDH(responder_priv, initiator_pub) = shared_secret
│
▼
┌─────────────────────────────────────────┐
│ HKDF-SHA256 │
│ - ikm: shared_secret │
│ - salt: IPK_group_key + TranscriptHash │
│ - info: "SessionKeys" │
│ - output: 48 bytes │
└─────────────────────────────────────────┘
│
├── I2RKey (16 bytes) - Initiator to Responder
├── R2IKey (16 bytes) - Responder to Initiator
└── AttestationChallenge (16 bytes)
All encrypted messages use AES-CCM with:
# Decryption (Matter_Frame.decrypt)
def decrypt()
var n = bytes(13)
n.add(self.sec_flags, 1)
n.add(self.message_counter, 4)
n .. self.source_node_id # or peer_node_id
crypto.AES_CCM.decrypt1(i2r, n, header, payload, tag)
end
┌─────────────────┐
│ No Session │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ PASE Session │ │ CASE Session │
│ (mode = 1) │ │ (mode = 2) │
│ - Temporary │ │ - Persistent │
│ - No fabric │ │ - Has fabric │
└────────┬────────┘ └────────┬────────┘
│ │
│ Commissioning │ Session
│ Complete │ Established
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Fabric Created │ │ Session Active │
│ - NOC assigned │ │ - Encrypted │
│ - IPK set │ │ - Resumable │
└────────┬────────┘ └─────────────────┘
│
│ CASE
│ Established
▼
┌─────────────────┐
│ CASE Session │
│ - Persistent │
│ - Resumable │
└─────────────────┘
/_matter_fabrics.json)[
{
"fabric_index": 1,
"fabric_id": "$base64_encoded",
"device_id": "$base64_encoded",
"fabric_compressed": "$base64_encoded",
"root_ca_certificate": "$base64_encoded",
"noc": "$base64_encoded",
"icac": "$base64_encoded",
"ipk_epoch_key": "$base64_encoded",
"no_private_key": "$base64_encoded",
"fabric_label": "Home",
"admin_subject": 12345,
"admin_vendor": 4996,
"_sessions": [
{
"local_session_id": 1234,
"initiator_session_id": 5678,
"counter_rcv": 100,
"counter_snd": 200,
"i2rkey": "$base64_encoded",
"r2ikey": "$base64_encoded",
"resumption_id": "$base64_encoded",
"shared_secret": "$base64_encoded"
}
]
}
]
Counter Management: Message counters prevent replay attacks
_COUNTER_SND_INCR = 1024)Session Expiration: Sessions can expire
Key Isolation: Each session has unique keys
Fabric Isolation: Each fabric has separate cryptographic material
This section documents known deviations from the Matter 1.0 specification found in the Tasmota implementation.
Location: Matter_Commissioning_Context.be, line 29
Spec says: "Matter PAKE V1 Commissioning"
Implementation uses: "CHIP PAKE V1 Commissioning"
# static Matter_Context_Prefix = "Matter PAKE V1 Commissioning" # spec is wrong
static var Matter_Context_Prefix = "CHIP PAKE V1 Commissioning" # from CHIP code
Reason: The Matter specification contains an error. The actual CHIP/Matter SDK implementation uses "CHIP PAKE V1 Commissioning" as the context prefix for SPAKE2+ transcript hashing. Using the spec-documented string would cause interoperability failures with all Matter controllers.
Impact: None - this is the correct behavior for interoperability.
Location: Matter_Commissioning_Context.be, lines 640-645
Spec says: Sigma3 TBSData signature MUST be verified, and session establishment MUST fail if invalid.
Implementation: Now correctly rejects invalid signatures:
if !sigma3_tbs_valid
log("MTR: sigma3_tbs does not have a valid signature", 2)
log("MTR: StatusReport(General Code: FAILURE, ProtocolId: SECURE_CHANNEL, ProtocolCode: INVALID_PARAMETER)", 2)
self.send_status_report(msg, 0x01, 0x0000, 0x0002, false)
return false
end
log("MTR: Sigma3 verified, computing new keys", 3)
Status: COMPLIANT - The implementation now properly validates the Sigma3 TBSData signature and rejects sessions with invalid signatures by sending a StatusReport with INVALID_PARAMETER error code.
Impact: None - fully compliant with Matter 1.0 specification.
Location: Matter_z_Commissioning.be, line 27
Spec says: Minimum 1000 iterations, recommended higher for security.
Implementation uses: Exactly 1000 iterations (the minimum)
static var PBKDF_ITERATIONS = 1000 # I don't see any reason to choose a different number
Impact: Compliant but uses minimum security. Higher iteration counts would provide better protection against brute-force attacks on the passcode.
Location: be_matter_counter.cpp, line 34
Spec says: Window size of 32 for message counter validation (Section 4.5.4)
Implementation uses: 32
#define MESSAGE_COUNTER_WINDOW_SIZE 32
Impact: None - fully compliant.
Location: be_matter_counter.cpp, line 133
Spec says: Complex rollover handling with specific rules for counter wrap-around.
Implementation does: Simplified handling that ignores rollover:
// in the past and out of the window (we ignore roll-over to simplify)
if (strict_mode) {
return bfalse;
} else {
c->counter = new_val;
c->window.reset();
return btrue;
}
Impact: Minor - could theoretically cause issues after 2^32 messages, but practically unlikely in embedded device scenarios.
Location: Various files using TLV.B1 vs TLV.B2
Observation: The implementation uses both B1 (1-byte length) and B2 (2-byte length) for octet strings. The choice appears to be based on expected data size rather than strict spec requirements.
Impact: None - TLV encoding is flexible and both are valid.
Location: Matter_Commissioning_Context.be
Spec says: Resumption ID should be cryptographically random 16 bytes.
Implementation: Uses crypto.random(16) which is compliant.
Impact: None - compliant.
MEDIUM PRIORITY: Consider increasing PBKDF2 iterations above the minimum 1000 for better security, especially for devices that may be exposed to network attacks.
LOW PRIORITY: The CHIP context prefix discrepancy is actually correct behavior - the spec documentation is wrong. Consider adding a comment explaining this more clearly.
Documentation generated: January 2026 Based on Tasmota Matter Berry implementation analysis