Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Matter fix fabric provisioning from CASE session for iOS 16.5 #18709

Merged
merged 1 commit into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
- AIThinker webcam issues (#18652)
- Berry `tasmota.wifi()` would wrongly report wifi as up
- Inverted shutter now reflect status also in WEBGUI and several minor fixes to make "inverted" consistant (#18701)
- Matter fix fabric provisioning from CASE session for iOS 16.5

### Removed

Expand Down
51 changes: 28 additions & 23 deletions lib/libesp32/berry_matter/src/embedded/Matter_Commissioning.be
Original file line number Diff line number Diff line change
Expand Up @@ -461,69 +461,74 @@ class Matter_Commisioning_Context
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", session.__future_local_session_id, msg.remote_ip, msg.remote_port), 2)

tasmota.log("MTR: fabric="+matter.inspect(session._fabric), 4)
tasmota.log("MTR: no_private_key="+session._fabric.no_private_key.tohex(), 4)
tasmota.log("MTR: noc ="+session._fabric.noc.tohex(), 4)
if session._fabric.get_icac()
tasmota.log("MTR: icac ="+session._fabric.get_icac().tohex(), 4)
tasmota.log("MTR: no_private_key="+session.get_pk().tohex(), 4)
tasmota.log("MTR: noc ="+session.get_noc().tohex(), 4)
if fabric.get_icac()
tasmota.log("MTR: icac ="+fabric.get_icac().tohex(), 4)
end
tasmota.log("MTR: root_ca_cert ="+session._fabric.root_ca_certificate.tohex(), 4)
tasmota.log("MTR: root_ca_cert ="+fabric.get_ca().tohex(), 4)

# Compute Sigma2, p.162
session.resumption_id = crypto.random(16)
session.resumption_id = crypto.random(16) # generate a new resumption id
session.__responder_priv = crypto.random(32)
session.__responder_pub = crypto.EC_P256().public_key(session.__responder_priv)
tasmota.log("MTR: ResponderEph_priv ="+session.__responder_priv.tohex(), 4)
tasmota.log("MTR: ResponderEph_pub ="+session.__responder_pub.tohex(), 4)
var responderRandom = crypto.random(32)

session.shared_secret = crypto.EC_P256().shared_key(session.__responder_priv, sigma1.initiatorEphPubKey)
tasmota.log("MTR: * shared_secret = " + session.shared_secret.tohex(), 4)

var sigma2_tbsdata = matter.TLV.Matter_TLV_struct()
sigma2_tbsdata.add_TLV(1, matter.TLV.B2, session.get_noc())
sigma2_tbsdata.add_TLV(2, matter.TLV.B2, session.get_icac())
sigma2_tbsdata.add_TLV(1, matter.TLV.B2, fabric.get_noc())
sigma2_tbsdata.add_TLV(2, matter.TLV.B2, fabric.get_icac())
sigma2_tbsdata.add_TLV(3, matter.TLV.B2, session.__responder_pub)
sigma2_tbsdata.add_TLV(4, matter.TLV.B2, sigma1.initiatorEphPubKey)

var TBSData2Signature = crypto.EC_P256().ecdsa_sign_sha256(session.get_pk(), sigma2_tbsdata.tlv2raw())
var TBSData2Signature = crypto.EC_P256().ecdsa_sign_sha256(fabric.get_pk(), sigma2_tbsdata.tlv2raw())
tasmota.log("****************************************", 4)
tasmota.log("MTR: * fabric.get_pk = " + str(fabric.get_pk()), 4)
tasmota.log("MTR: * sigma2_tbsdata = " + str(sigma2_tbsdata), 4)
tasmota.log("MTR: * TBSData2Signature = " + TBSData2Signature.tohex(), 4)

var sigma2_tbedata = matter.TLV.Matter_TLV_struct()
sigma2_tbedata.add_TLV(1, matter.TLV.B2, session.get_noc())
sigma2_tbedata.add_TLV(2, matter.TLV.B2, session.get_icac())
sigma2_tbedata.add_TLV(1, matter.TLV.B2, fabric.get_noc())
sigma2_tbedata.add_TLV(2, matter.TLV.B2, fabric.get_icac())
sigma2_tbedata.add_TLV(3, matter.TLV.B2, TBSData2Signature)
sigma2_tbedata.add_TLV(4, matter.TLV.B2, session.resumption_id)

# compute TranscriptHash = Crypto_Hash(message = Msg1)
# tasmota.log("****************************************", 4)
tasmota.log("****************************************", 4)
session.__Msg1 = sigma1.Msg1
# tasmota.log("MTR: * resumptionid = " + session.resumption_id.tohex(), 4)
# tasmota.log("MTR: * MSG1 = " + session.__Msg1.tohex(), 4)
tasmota.log("MTR: * resumptionid = " + session.resumption_id.tohex(), 4)
tasmota.log("MTR: * MSG1 = " + session.__Msg1.tohex(), 4)
var TranscriptHash = crypto.SHA256().update(session.__Msg1).out()
# tasmota.log("MTR: TranscriptHash =" + TranscriptHash.tohex(), 4)
tasmota.log("MTR: TranscriptHash =" + TranscriptHash.tohex(), 4)

# Compute S2K, p.175
var s2k_info = bytes().fromstring(self.S2K_Info)
var s2k_salt = session.get_ipk_group_key() + responderRandom + session.__responder_pub + TranscriptHash
var s2k_salt = fabric.get_ipk_group_key() + responderRandom + session.__responder_pub + TranscriptHash

var s2k = crypto.HKDF_SHA256().derive(session.shared_secret, s2k_salt, s2k_info, 16)
# tasmota.log("MTR: * SharedSecret = " + session.shared_secret.tohex(), 4)
# tasmota.log("MTR: * s2k_salt = " + s2k_salt.tohex(), 4)
# tasmota.log("MTR: * s2k = " + s2k.tohex(), 4)
tasmota.log("MTR: * SharedSecret = " + session.shared_secret.tohex(), 4)
tasmota.log("MTR: * s2k_salt = " + s2k_salt.tohex(), 4)
tasmota.log("MTR: * s2k = " + s2k.tohex(), 4)

var sigma2_tbedata_raw = sigma2_tbedata.tlv2raw()
# tasmota.log("MTR: * TBEData2Raw = " + sigma2_tbedata_raw.tohex(), 4)
tasmota.log("MTR: * TBEData2Raw = " + sigma2_tbedata_raw.tohex(), 4)
# // `AES_CCM.init(secret_key:bytes(16 or 32), iv:bytes(7..13), aad:bytes(), data_len:int, tag_len:int) -> instance`

var aes = crypto.AES_CCM(s2k, bytes().fromstring(self.TBEData2_Nonce), bytes(), size(sigma2_tbedata_raw), 16)
var TBEData2Encrypted = aes.encrypt(sigma2_tbedata_raw) + aes.tag()
# tasmota.log("MTR: * TBEData2Enc = " + TBEData2Encrypted.tohex(), 4)
# tasmota.log("****************************************", 4)
tasmota.log("MTR: * TBEData2Enc = " + TBEData2Encrypted.tohex(), 4)
tasmota.log("****************************************", 4)

var sigma2 = matter.Sigma2()
sigma2.responderRandom = responderRandom
sigma2.responderSessionId = session.__future_local_session_id
sigma2.responderEphPubKey = session.__responder_pub
sigma2.encrypted2 = TBEData2Encrypted
# tasmota.log("MTR: sigma2: " + matter.inspect(sigma2), 4)
tasmota.log("MTR: sigma2: " + matter.inspect(sigma2), 4)
var sigma2_raw = sigma2.tlv2raw()
session.__Msg2 = sigma2_raw
# tasmota.log("MTR: sigma2_raw: " + sigma2_raw.tohex(), 4)
Expand Down
11 changes: 4 additions & 7 deletions lib/libesp32/berry_matter/src/embedded/Matter_Device.be
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,9 @@ class Matter_Device
# Start Operational Discovery for this session
#
# Deferred until next tick.
def start_operational_discovery_deferred(session)
def start_operational_discovery_deferred(fabric)
# defer to next click
tasmota.set_timer(0, /-> self.start_operational_discovery(session))
tasmota.set_timer(0, /-> self.start_operational_discovery(fabric))
end

#############################################################
Expand All @@ -400,7 +400,7 @@ class Matter_Device
#
# Stop Basic Commissioning and clean PASE specific values (to save memory).
# Announce fabric entry in mDNS.
def start_operational_discovery(session)
def start_operational_discovery(fabric)
import crypto
import mdns
import string
Expand All @@ -411,10 +411,7 @@ class Matter_Device
# self.root_w1 = nil
self.root_L = nil

# we keep the PASE session for 1 minute
session.set_expire_in_seconds(60)

self.mdns_announce_op_discovery(session.get_fabric())
self.mdns_announce_op_discovery(fabric)
end

#############################################################
Expand Down
57 changes: 57 additions & 0 deletions lib/libesp32/berry_matter/src/embedded/Matter_Fabric.be
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,54 @@ class Matter_Fabric : Matter_Expirable
def get_fabric_index() return self.fabric_index end

def set_fabric_index(v) self.fabric_index = v end
def set_ca(ca)
self.root_ca_certificate = ca
end
def set_noc_icac(noc, icac)
self.noc = noc
self.icac = icac
end
def set_ipk_epoch_key(ipk_epoch_key)
self.ipk_epoch_key = ipk_epoch_key
end
def set_admin_subject_vendor(admin_subject, admin_vendor)
self.admin_subject = admin_subject
self.admin_vendor = admin_vendor
end

def set_fabric_device(fabric_id, device_id, fc, fabric_parent)
self.fabric_id = fabric_id
self.device_id = device_id
self.fabric_compressed = fc
self.fabric_parent = (fabric_parent != nil) ? fabric_parent.get_fabric_index() : nil
end

#############################################################
# Generate a private key (or retrieve it)
#
# PK is generated by a commissioning session
def get_pk()
return self.no_private_key
end
def set_pk(pk)
self.no_private_key = pk
end

#############################################################
# Register the frabric as complete (end of commissioning)
def fabric_candidate()
self.set_expire_in_seconds(120) # expire in 2 minutes
self.assign_fabric_index()
self._store.add_fabric(self)
end

#############################################################
# Assign a new fabric index
def assign_fabric_index()
if (self.get_fabric_index() == nil)
self.set_fabric_index(self._store.next_fabric_idx())
end
end

#############################################################
# When hydrating from persistance, update counters
Expand All @@ -109,6 +157,15 @@ class Matter_Fabric : Matter_Expirable
self.counter_group_ctrl_snd = self._counter_group_ctrl_snd_impl.val()
end

#############################################################
# Register the fabric as complete (end of commissioning)
def fabric_completed()
self.set_no_expiration()
self.set_persist(true)
self.assign_fabric_index()
self._store.add_fabric(self)
end

#############################################################
# Management of security counters
#############################################################
Expand Down
3 changes: 2 additions & 1 deletion lib/libesp32/berry_matter/src/embedded/Matter_IM.be
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,10 @@ class Matter_IM
ctx.status = matter.UNSUPPORTED_COMMAND #default error if returned `nil`

var cmd_name = matter.get_command_name(ctx.cluster, ctx.command)
var ctx_str = str(ctx) # keep string before invoking, it is modified by response
var res = self.device.invoke_request(msg.session, q.command_fields, ctx)
var params_log = (ctx.log != nil) ? "(" + str(ctx.log) + ") " : ""
tasmota.log(string.format("MTR: >Command (%6i) %s %s %s", msg.session.local_session_id, str(ctx), cmd_name ? cmd_name : "", params_log), 2)
tasmota.log(string.format("MTR: >Command (%6i) %s %s %s", msg.session.local_session_id, ctx_str, cmd_name ? cmd_name : "", params_log), 2)
ctx.log = nil
var a1 = matter.InvokeResponseIB()
if res == true || ctx.status == matter.SUCCESS # special case, just respond ok
Expand Down
96 changes: 63 additions & 33 deletions lib/libesp32/berry_matter/src/embedded/Matter_Plugin_Root.be
Original file line number Diff line number Diff line change
Expand Up @@ -362,22 +362,26 @@ class Matter_Plugin_Root : Matter_Plugin

elif command == 0x0004 # ---------- CommissioningComplete p.636 ----------
# no data
session._breadcrumb = 0 # clear breadcrumb
session.fabric_completed() # fabric information is complete, persist
session.set_no_expiration()
session.save()

# create CommissioningCompleteResponse
# ID=1
# 0=ErrorCode (OK=0)
# 1=DebugText
var ccr = TLV.Matter_TLV_struct()
ccr.add_TLV(0, TLV.U1, 0) # ErrorCode = OK
ccr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
ctx.command = 0x05 # CommissioningCompleteResponse

self.device.start_commissioning_complete_deferred(session)
return ccr
if session._fabric
session._breadcrumb = 0 # clear breadcrumb
session._fabric.fabric_completed() # fabric information is complete, persist
session.set_no_expiration()
session.save()

# create CommissioningCompleteResponse
# ID=1
# 0=ErrorCode (OK=0)
# 1=DebugText
var ccr = TLV.Matter_TLV_struct()
ccr.add_TLV(0, TLV.U1, 0) # ErrorCode = OK
ccr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
ctx.command = 0x05 # CommissioningCompleteResponse

self.device.start_commissioning_complete_deferred(session)
return ccr
else
raise "context_error", "CommissioningComplete: no fabric attached"
end
end

elif cluster == 0x003E # ========== Node Operational Credentials Cluster 11.17 p.704 ==========
Expand Down Expand Up @@ -427,6 +431,7 @@ class Matter_Plugin_Root : Matter_Plugin
var CSRNonce = val.findsubval(0) # octstr 32
if size(CSRNonce) != 32 return nil end # check size on nonce
var IsForUpdateNOC = val.findsubval(1, false) # bool
tasmota.log(string.format("MTR: CSRRequest CSRNonce=%s IsForUpdateNOC=%s", str(CSRNonce), str(IsForUpdateNOC)), 3)

var csr = session.gen_CSR()

Expand All @@ -450,34 +455,47 @@ class Matter_Plugin_Root : Matter_Plugin

elif command == 0x000B # ---------- AddTrustedRootCertificate ----------
var RootCACertificate = val.findsubval(0) # octstr 400 max
session.set_ca(RootCACertificate)
# TODO - additional tests are expected according to 11.17.7.13. AddTrustedRootCertificate Command
session.set_temp_ca(RootCACertificate)
tasmota.log("MTR: received ca_root="+RootCACertificate.tohex(), 3)
ctx.status = matter.SUCCESS # OK
return nil # trigger a standalone ack

elif command == 0x0006 # ---------- AddNOC ----------
tasmota.log("MTR: AddNoc Args=" + str(val), 3)
var NOCValue = val.findsubval(0) # octstr max 400
var ICACValue = val.findsubval(1) # octstr max 400
# Apple sends an empty ICAC instead of a missing attribute, fix this
if size(ICACValue) == 0 ICACValue = nil end
var IpkValue = val.findsubval(2) # octstr max 16
var CaseAdminSubject = val.findsubval(3)
var AdminVendorId = val.findsubval(4)
# tasmota.log("MTR: AddNoc NOCValue=" + (NOCValue ? NOCValue.tohex() : ""), 3)
# tasmota.log("MTR: AddNoc ICACValue=" + (ICACValue ? ICACValue.tohex() : ""), 3)
# tasmota.log("MTR: AddNoc IpkValue=" + str(IpkValue), 3)
# tasmota.log("MTR: AddNoc CaseAdminSubject=" + str(CaseAdminSubject), 3)
# tasmota.log("MTR: AddNoc AdminVendorId=" + str(AdminVendorId), 3)

if session.get_ca() == nil
if session.get_temp_ca() == nil
tasmota.log("MTR: Error: AdNOC without CA", 2)
return nil
end

session.set_noc(NOCValue, ICACValue)
session.set_ipk_epoch_key(IpkValue)
session.set_admin_subject_vendor(CaseAdminSubject, AdminVendorId)
var new_fabric = self.device.sessions.create_fabric()
new_fabric.set_ca(session.get_temp_ca()) # copy temporary CA to fabric
new_fabric.set_noc_icac(NOCValue, ICACValue)
new_fabric.set_ipk_epoch_key(IpkValue)
new_fabric.set_admin_subject_vendor(CaseAdminSubject, AdminVendorId)
new_fabric.set_pk(session.get_pk()) # copy the temporary commissioning PK to the fabric

# extract important information from NOC
var noc_cert = matter.TLV.parse(NOCValue)
var dnlist = noc_cert.findsub(6)
var fabric_id = dnlist.findsubval(21)
var deviceid = dnlist.findsubval(17)
# tasmota.log("MTR: AddNoc noc_cert=" + str(noc_cert), 3)
# tasmota.log("MTR: AddNoc dnlist=" + str(dnlist), 3)

if !fabric_id || !deviceid
tasmota.log("MTR: Error: no fabricid nor deviceid in NOC certificate", 2)
return false
Expand All @@ -486,25 +504,37 @@ class Matter_Plugin_Root : Matter_Plugin
if type(fabric_id) == 'int' fabric_id = int64.fromu32(fabric_id).tobytes() else fabric_id = fabric_id.tobytes() end
if type(deviceid) == 'int' deviceid = int64.fromu32(deviceid).tobytes() else deviceid = deviceid.tobytes() end

var root_ca = matter.TLV.parse(session.get_ca()).findsubval(9) # extract public key from ca
root_ca = root_ca[1..] # remove first byte as per Matter specification
# tasmota.log("MTR: AddNoc fabric_id=" + str(fabric_id), 3)
# tasmota.log("MTR: AddNoc deviceid=" + str(deviceid), 3)

var root_ca_pub = session.get_temp_ca_pub()
# tasmota.log("MTR: AddNoc root_ca_pub=" + str(root_ca_pub), 3)
# tasmota.log("MTR: AddNoc root_ca_pub=" + root_ca_pub.tohex(), 3)
root_ca_pub = root_ca_pub[1..] # remove first byte as per Matter specification
var info = bytes().fromstring("CompressedFabric") # as per spec, 4.3.2.2 p.99
var hk = crypto.HKDF_SHA256()
var fabric_rev = fabric_id.copy().reverse()
var k_fabric = hk.derive(root_ca, fabric_rev, info, 8)
session.set_fabric_device(fabric_id, deviceid, k_fabric, self.device.commissioning_admin_fabric)
var k_fabric = hk.derive(root_ca_pub, fabric_rev, info, 8)
var parent_fabric = session._fabric ? session._fabric : self.device.commissioning_admin_fabric # get parent fabric whether CASE or PASE
new_fabric.set_fabric_device(fabric_id, deviceid, k_fabric, parent_fabric)

# tasmota.log("MTR: AddNoc k_fabric=" + str(k_fabric), 3)
# We have a candidate fabric, add it as expirable for 2 minutes
session.persist_to_fabric() # fabric object is completed, persist it
session.fabric_candidate()
new_fabric.fabric_candidate()

# move to next step
self.device.start_operational_discovery_deferred(session)
# session.fabric_completed()
tasmota.log("MTR: ------------------------------------------", 3)
tasmota.log("MTR: fabric=" + matter.inspect(session._fabric), 3)
tasmota.log("MTR: ------------------------------------------", 3)
session._fabric.log_new_fabric() # log that we registered a new fabric
self.device.start_operational_discovery_deferred(new_fabric)

# we keep the PASE session for 1 minute
if session.is_PASE()
session.set_expire_in_seconds(60)
end

# tasmota.log("MTR: ------------------------------------------", 3)
# tasmota.log("MTR: session=" + matter.inspect(session), 3)
# tasmota.log("MTR: fabric=" + matter.inspect(session._fabric), 3)
# tasmota.log("MTR: ------------------------------------------", 3)
new_fabric.log_new_fabric() # log that we registered a new fabric
# create NOCResponse
# 0=StatusCode
# 1=FabricIndex (1-254) (opt)
Expand Down
Loading