Skip to content

Commit c1ec35a

Browse files
authored
Matter fix fabric provisioning from CASE session for iOS 16.5 (#18709)
1 parent 2c8f9f8 commit c1ec35a

13 files changed

+3671
-3371
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
2323
- AIThinker webcam issues (#18652)
2424
- Berry `tasmota.wifi()` would wrongly report wifi as up
2525
- Inverted shutter now reflect status also in WEBGUI and several minor fixes to make "inverted" consistant (#18701)
26+
- Matter fix fabric provisioning from CASE session for iOS 16.5
2627

2728
### Removed
2829

lib/libesp32/berry_matter/src/embedded/Matter_Commissioning.be

+28-23
Original file line numberDiff line numberDiff line change
@@ -461,69 +461,74 @@ class Matter_Commisioning_Context
461461
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", session.__future_local_session_id, msg.remote_ip, msg.remote_port), 2)
462462

463463
tasmota.log("MTR: fabric="+matter.inspect(session._fabric), 4)
464-
tasmota.log("MTR: no_private_key="+session._fabric.no_private_key.tohex(), 4)
465-
tasmota.log("MTR: noc ="+session._fabric.noc.tohex(), 4)
466-
if session._fabric.get_icac()
467-
tasmota.log("MTR: icac ="+session._fabric.get_icac().tohex(), 4)
464+
tasmota.log("MTR: no_private_key="+session.get_pk().tohex(), 4)
465+
tasmota.log("MTR: noc ="+session.get_noc().tohex(), 4)
466+
if fabric.get_icac()
467+
tasmota.log("MTR: icac ="+fabric.get_icac().tohex(), 4)
468468
end
469-
tasmota.log("MTR: root_ca_cert ="+session._fabric.root_ca_certificate.tohex(), 4)
469+
tasmota.log("MTR: root_ca_cert ="+fabric.get_ca().tohex(), 4)
470470

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

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

481482
var sigma2_tbsdata = matter.TLV.Matter_TLV_struct()
482-
sigma2_tbsdata.add_TLV(1, matter.TLV.B2, session.get_noc())
483-
sigma2_tbsdata.add_TLV(2, matter.TLV.B2, session.get_icac())
483+
sigma2_tbsdata.add_TLV(1, matter.TLV.B2, fabric.get_noc())
484+
sigma2_tbsdata.add_TLV(2, matter.TLV.B2, fabric.get_icac())
484485
sigma2_tbsdata.add_TLV(3, matter.TLV.B2, session.__responder_pub)
485486
sigma2_tbsdata.add_TLV(4, matter.TLV.B2, sigma1.initiatorEphPubKey)
486487

487-
var TBSData2Signature = crypto.EC_P256().ecdsa_sign_sha256(session.get_pk(), sigma2_tbsdata.tlv2raw())
488+
var TBSData2Signature = crypto.EC_P256().ecdsa_sign_sha256(fabric.get_pk(), sigma2_tbsdata.tlv2raw())
489+
tasmota.log("****************************************", 4)
490+
tasmota.log("MTR: * fabric.get_pk = " + str(fabric.get_pk()), 4)
491+
tasmota.log("MTR: * sigma2_tbsdata = " + str(sigma2_tbsdata), 4)
492+
tasmota.log("MTR: * TBSData2Signature = " + TBSData2Signature.tohex(), 4)
488493

489494
var sigma2_tbedata = matter.TLV.Matter_TLV_struct()
490-
sigma2_tbedata.add_TLV(1, matter.TLV.B2, session.get_noc())
491-
sigma2_tbedata.add_TLV(2, matter.TLV.B2, session.get_icac())
495+
sigma2_tbedata.add_TLV(1, matter.TLV.B2, fabric.get_noc())
496+
sigma2_tbedata.add_TLV(2, matter.TLV.B2, fabric.get_icac())
492497
sigma2_tbedata.add_TLV(3, matter.TLV.B2, TBSData2Signature)
493498
sigma2_tbedata.add_TLV(4, matter.TLV.B2, session.resumption_id)
494499

495500
# compute TranscriptHash = Crypto_Hash(message = Msg1)
496-
# tasmota.log("****************************************", 4)
501+
tasmota.log("****************************************", 4)
497502
session.__Msg1 = sigma1.Msg1
498-
# tasmota.log("MTR: * resumptionid = " + session.resumption_id.tohex(), 4)
499-
# tasmota.log("MTR: * MSG1 = " + session.__Msg1.tohex(), 4)
503+
tasmota.log("MTR: * resumptionid = " + session.resumption_id.tohex(), 4)
504+
tasmota.log("MTR: * MSG1 = " + session.__Msg1.tohex(), 4)
500505
var TranscriptHash = crypto.SHA256().update(session.__Msg1).out()
501-
# tasmota.log("MTR: TranscriptHash =" + TranscriptHash.tohex(), 4)
506+
tasmota.log("MTR: TranscriptHash =" + TranscriptHash.tohex(), 4)
502507

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

507512
var s2k = crypto.HKDF_SHA256().derive(session.shared_secret, s2k_salt, s2k_info, 16)
508-
# tasmota.log("MTR: * SharedSecret = " + session.shared_secret.tohex(), 4)
509-
# tasmota.log("MTR: * s2k_salt = " + s2k_salt.tohex(), 4)
510-
# tasmota.log("MTR: * s2k = " + s2k.tohex(), 4)
513+
tasmota.log("MTR: * SharedSecret = " + session.shared_secret.tohex(), 4)
514+
tasmota.log("MTR: * s2k_salt = " + s2k_salt.tohex(), 4)
515+
tasmota.log("MTR: * s2k = " + s2k.tohex(), 4)
511516

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

516521
var aes = crypto.AES_CCM(s2k, bytes().fromstring(self.TBEData2_Nonce), bytes(), size(sigma2_tbedata_raw), 16)
517522
var TBEData2Encrypted = aes.encrypt(sigma2_tbedata_raw) + aes.tag()
518-
# tasmota.log("MTR: * TBEData2Enc = " + TBEData2Encrypted.tohex(), 4)
519-
# tasmota.log("****************************************", 4)
523+
tasmota.log("MTR: * TBEData2Enc = " + TBEData2Encrypted.tohex(), 4)
524+
tasmota.log("****************************************", 4)
520525

521526
var sigma2 = matter.Sigma2()
522527
sigma2.responderRandom = responderRandom
523528
sigma2.responderSessionId = session.__future_local_session_id
524529
sigma2.responderEphPubKey = session.__responder_pub
525530
sigma2.encrypted2 = TBEData2Encrypted
526-
# tasmota.log("MTR: sigma2: " + matter.inspect(sigma2), 4)
531+
tasmota.log("MTR: sigma2: " + matter.inspect(sigma2), 4)
527532
var sigma2_raw = sigma2.tlv2raw()
528533
session.__Msg2 = sigma2_raw
529534
# tasmota.log("MTR: sigma2_raw: " + sigma2_raw.tohex(), 4)

lib/libesp32/berry_matter/src/embedded/Matter_Device.be

+4-7
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,9 @@ class Matter_Device
381381
# Start Operational Discovery for this session
382382
#
383383
# Deferred until next tick.
384-
def start_operational_discovery_deferred(session)
384+
def start_operational_discovery_deferred(fabric)
385385
# defer to next click
386-
tasmota.set_timer(0, /-> self.start_operational_discovery(session))
386+
tasmota.set_timer(0, /-> self.start_operational_discovery(fabric))
387387
end
388388

389389
#############################################################
@@ -400,7 +400,7 @@ class Matter_Device
400400
#
401401
# Stop Basic Commissioning and clean PASE specific values (to save memory).
402402
# Announce fabric entry in mDNS.
403-
def start_operational_discovery(session)
403+
def start_operational_discovery(fabric)
404404
import crypto
405405
import mdns
406406
import string
@@ -411,10 +411,7 @@ class Matter_Device
411411
# self.root_w1 = nil
412412
self.root_L = nil
413413

414-
# we keep the PASE session for 1 minute
415-
session.set_expire_in_seconds(60)
416-
417-
self.mdns_announce_op_discovery(session.get_fabric())
414+
self.mdns_announce_op_discovery(fabric)
418415
end
419416

420417
#############################################################

lib/libesp32/berry_matter/src/embedded/Matter_Fabric.be

+57
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,54 @@ class Matter_Fabric : Matter_Expirable
9696
def get_fabric_index() return self.fabric_index end
9797

9898
def set_fabric_index(v) self.fabric_index = v end
99+
def set_ca(ca)
100+
self.root_ca_certificate = ca
101+
end
102+
def set_noc_icac(noc, icac)
103+
self.noc = noc
104+
self.icac = icac
105+
end
106+
def set_ipk_epoch_key(ipk_epoch_key)
107+
self.ipk_epoch_key = ipk_epoch_key
108+
end
109+
def set_admin_subject_vendor(admin_subject, admin_vendor)
110+
self.admin_subject = admin_subject
111+
self.admin_vendor = admin_vendor
112+
end
113+
114+
def set_fabric_device(fabric_id, device_id, fc, fabric_parent)
115+
self.fabric_id = fabric_id
116+
self.device_id = device_id
117+
self.fabric_compressed = fc
118+
self.fabric_parent = (fabric_parent != nil) ? fabric_parent.get_fabric_index() : nil
119+
end
120+
121+
#############################################################
122+
# Generate a private key (or retrieve it)
123+
#
124+
# PK is generated by a commissioning session
125+
def get_pk()
126+
return self.no_private_key
127+
end
128+
def set_pk(pk)
129+
self.no_private_key = pk
130+
end
131+
132+
#############################################################
133+
# Register the frabric as complete (end of commissioning)
134+
def fabric_candidate()
135+
self.set_expire_in_seconds(120) # expire in 2 minutes
136+
self.assign_fabric_index()
137+
self._store.add_fabric(self)
138+
end
139+
140+
#############################################################
141+
# Assign a new fabric index
142+
def assign_fabric_index()
143+
if (self.get_fabric_index() == nil)
144+
self.set_fabric_index(self._store.next_fabric_idx())
145+
end
146+
end
99147

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

160+
#############################################################
161+
# Register the fabric as complete (end of commissioning)
162+
def fabric_completed()
163+
self.set_no_expiration()
164+
self.set_persist(true)
165+
self.assign_fabric_index()
166+
self._store.add_fabric(self)
167+
end
168+
112169
#############################################################
113170
# Management of security counters
114171
#############################################################

lib/libesp32/berry_matter/src/embedded/Matter_IM.be

+2-1
Original file line numberDiff line numberDiff line change
@@ -402,9 +402,10 @@ class Matter_IM
402402
ctx.status = matter.UNSUPPORTED_COMMAND #default error if returned `nil`
403403

404404
var cmd_name = matter.get_command_name(ctx.cluster, ctx.command)
405+
var ctx_str = str(ctx) # keep string before invoking, it is modified by response
405406
var res = self.device.invoke_request(msg.session, q.command_fields, ctx)
406407
var params_log = (ctx.log != nil) ? "(" + str(ctx.log) + ") " : ""
407-
tasmota.log(string.format("MTR: >Command (%6i) %s %s %s", msg.session.local_session_id, str(ctx), cmd_name ? cmd_name : "", params_log), 2)
408+
tasmota.log(string.format("MTR: >Command (%6i) %s %s %s", msg.session.local_session_id, ctx_str, cmd_name ? cmd_name : "", params_log), 2)
408409
ctx.log = nil
409410
var a1 = matter.InvokeResponseIB()
410411
if res == true || ctx.status == matter.SUCCESS # special case, just respond ok

lib/libesp32/berry_matter/src/embedded/Matter_Plugin_Root.be

+63-33
Original file line numberDiff line numberDiff line change
@@ -362,22 +362,26 @@ class Matter_Plugin_Root : Matter_Plugin
362362

363363
elif command == 0x0004 # ---------- CommissioningComplete p.636 ----------
364364
# no data
365-
session._breadcrumb = 0 # clear breadcrumb
366-
session.fabric_completed() # fabric information is complete, persist
367-
session.set_no_expiration()
368-
session.save()
369-
370-
# create CommissioningCompleteResponse
371-
# ID=1
372-
# 0=ErrorCode (OK=0)
373-
# 1=DebugText
374-
var ccr = TLV.Matter_TLV_struct()
375-
ccr.add_TLV(0, TLV.U1, 0) # ErrorCode = OK
376-
ccr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
377-
ctx.command = 0x05 # CommissioningCompleteResponse
378-
379-
self.device.start_commissioning_complete_deferred(session)
380-
return ccr
365+
if session._fabric
366+
session._breadcrumb = 0 # clear breadcrumb
367+
session._fabric.fabric_completed() # fabric information is complete, persist
368+
session.set_no_expiration()
369+
session.save()
370+
371+
# create CommissioningCompleteResponse
372+
# ID=1
373+
# 0=ErrorCode (OK=0)
374+
# 1=DebugText
375+
var ccr = TLV.Matter_TLV_struct()
376+
ccr.add_TLV(0, TLV.U1, 0) # ErrorCode = OK
377+
ccr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
378+
ctx.command = 0x05 # CommissioningCompleteResponse
379+
380+
self.device.start_commissioning_complete_deferred(session)
381+
return ccr
382+
else
383+
raise "context_error", "CommissioningComplete: no fabric attached"
384+
end
381385
end
382386

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

431436
var csr = session.gen_CSR()
432437

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

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

458464
elif command == 0x0006 # ---------- AddNOC ----------
465+
tasmota.log("MTR: AddNoc Args=" + str(val), 3)
459466
var NOCValue = val.findsubval(0) # octstr max 400
460467
var ICACValue = val.findsubval(1) # octstr max 400
461468
# Apple sends an empty ICAC instead of a missing attribute, fix this
462469
if size(ICACValue) == 0 ICACValue = nil end
463470
var IpkValue = val.findsubval(2) # octstr max 16
464471
var CaseAdminSubject = val.findsubval(3)
465472
var AdminVendorId = val.findsubval(4)
473+
# tasmota.log("MTR: AddNoc NOCValue=" + (NOCValue ? NOCValue.tohex() : ""), 3)
474+
# tasmota.log("MTR: AddNoc ICACValue=" + (ICACValue ? ICACValue.tohex() : ""), 3)
475+
# tasmota.log("MTR: AddNoc IpkValue=" + str(IpkValue), 3)
476+
# tasmota.log("MTR: AddNoc CaseAdminSubject=" + str(CaseAdminSubject), 3)
477+
# tasmota.log("MTR: AddNoc AdminVendorId=" + str(AdminVendorId), 3)
466478

467-
if session.get_ca() == nil
479+
if session.get_temp_ca() == nil
468480
tasmota.log("MTR: Error: AdNOC without CA", 2)
469481
return nil
470482
end
471483

472-
session.set_noc(NOCValue, ICACValue)
473-
session.set_ipk_epoch_key(IpkValue)
474-
session.set_admin_subject_vendor(CaseAdminSubject, AdminVendorId)
484+
var new_fabric = self.device.sessions.create_fabric()
485+
new_fabric.set_ca(session.get_temp_ca()) # copy temporary CA to fabric
486+
new_fabric.set_noc_icac(NOCValue, ICACValue)
487+
new_fabric.set_ipk_epoch_key(IpkValue)
488+
new_fabric.set_admin_subject_vendor(CaseAdminSubject, AdminVendorId)
489+
new_fabric.set_pk(session.get_pk()) # copy the temporary commissioning PK to the fabric
475490

476491
# extract important information from NOC
477492
var noc_cert = matter.TLV.parse(NOCValue)
478493
var dnlist = noc_cert.findsub(6)
479494
var fabric_id = dnlist.findsubval(21)
480495
var deviceid = dnlist.findsubval(17)
496+
# tasmota.log("MTR: AddNoc noc_cert=" + str(noc_cert), 3)
497+
# tasmota.log("MTR: AddNoc dnlist=" + str(dnlist), 3)
498+
481499
if !fabric_id || !deviceid
482500
tasmota.log("MTR: Error: no fabricid nor deviceid in NOC certificate", 2)
483501
return false
@@ -486,25 +504,37 @@ class Matter_Plugin_Root : Matter_Plugin
486504
if type(fabric_id) == 'int' fabric_id = int64.fromu32(fabric_id).tobytes() else fabric_id = fabric_id.tobytes() end
487505
if type(deviceid) == 'int' deviceid = int64.fromu32(deviceid).tobytes() else deviceid = deviceid.tobytes() end
488506

489-
var root_ca = matter.TLV.parse(session.get_ca()).findsubval(9) # extract public key from ca
490-
root_ca = root_ca[1..] # remove first byte as per Matter specification
507+
# tasmota.log("MTR: AddNoc fabric_id=" + str(fabric_id), 3)
508+
# tasmota.log("MTR: AddNoc deviceid=" + str(deviceid), 3)
509+
510+
var root_ca_pub = session.get_temp_ca_pub()
511+
# tasmota.log("MTR: AddNoc root_ca_pub=" + str(root_ca_pub), 3)
512+
# tasmota.log("MTR: AddNoc root_ca_pub=" + root_ca_pub.tohex(), 3)
513+
root_ca_pub = root_ca_pub[1..] # remove first byte as per Matter specification
491514
var info = bytes().fromstring("CompressedFabric") # as per spec, 4.3.2.2 p.99
492515
var hk = crypto.HKDF_SHA256()
493516
var fabric_rev = fabric_id.copy().reverse()
494-
var k_fabric = hk.derive(root_ca, fabric_rev, info, 8)
495-
session.set_fabric_device(fabric_id, deviceid, k_fabric, self.device.commissioning_admin_fabric)
517+
var k_fabric = hk.derive(root_ca_pub, fabric_rev, info, 8)
518+
var parent_fabric = session._fabric ? session._fabric : self.device.commissioning_admin_fabric # get parent fabric whether CASE or PASE
519+
new_fabric.set_fabric_device(fabric_id, deviceid, k_fabric, parent_fabric)
496520

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

501525
# move to next step
502-
self.device.start_operational_discovery_deferred(session)
503-
# session.fabric_completed()
504-
tasmota.log("MTR: ------------------------------------------", 3)
505-
tasmota.log("MTR: fabric=" + matter.inspect(session._fabric), 3)
506-
tasmota.log("MTR: ------------------------------------------", 3)
507-
session._fabric.log_new_fabric() # log that we registered a new fabric
526+
self.device.start_operational_discovery_deferred(new_fabric)
527+
528+
# we keep the PASE session for 1 minute
529+
if session.is_PASE()
530+
session.set_expire_in_seconds(60)
531+
end
532+
533+
# tasmota.log("MTR: ------------------------------------------", 3)
534+
# tasmota.log("MTR: session=" + matter.inspect(session), 3)
535+
# tasmota.log("MTR: fabric=" + matter.inspect(session._fabric), 3)
536+
# tasmota.log("MTR: ------------------------------------------", 3)
537+
new_fabric.log_new_fabric() # log that we registered a new fabric
508538
# create NOCResponse
509539
# 0=StatusCode
510540
# 1=FabricIndex (1-254) (opt)

0 commit comments

Comments
 (0)