I am trying to build my own .Net Matter Controller.
I’m writing these posts as I go, figuring the stuff out and blogging how and what I’m doing.
In my previous post, I got as far as the final PASE message, but no matter (!) what I did, the response was just never accepted. Something was wrong with the cryptography code I had written.
What I needed was a way to compare what the device itself was doing. If I could see what values the device was generating, it would help me fix my own code. I tried to add logging to the ESP32, but that wasn’t logging enough.
Then I thought about matter.js, which a pure Javascript implementation of Matter. It can act as both a commissioner or a device. Adding logging to Javascript is usually simple, so I figured this was worth a shot.
First attempt at pairing
To start, I pulled down the matter.js from from https://github.com/project-chip/matter.js
I complied it, following the instructions.
After a little poking around, I got it to act as a device, with Bluetooth Discovery. This took some tweaks. First, I had to change the package.json, adding a new script to launch the device-onff-advanced example. I needed this particular example as it offered Bluetooth.
"matter-device-advanced": "matter-run packages/examples/src/device-onoff-advanced/DeviceNodeFull.ts",
With that added, I launched the device using this command
npm run matter-device-advanced -- --ble-enable
Once the device was up and running, I launched my Controller console app.
It found the device via Bluetooth and exchanged the first two PASE message, but then bombed out!!!

It seems to say that my BTP acknowledgment was out of sequence.
I realised that I wasn’t acknowledging the Handshake response! This is why the AckNumber was 1. That wasn’t correct. In the specification:
Because the server’s handshake response bears an implicit BTP sequence number of zero, a server
SHALL start its acknowledgement-received timer when it sends a handshake response.
I modified my GetSegments method
if (segments.Count == 0)
{
segment.ControlFlags = BTPControlFlags.Beginning;
segment.MessageLength = (ushort)messageBytes.Length;
headerLength += 2; // Add two bytes to the header length to indicate we have the MessageLength.
// If we have any outstanding messages to acknowledges, add it here!
//
if (_acknowledgedSequenceCount != _receivedSequenceCount)
{
_acknowledgedSequenceCount = _receivedSequenceCount;
segment.AcknowledgeNumber = (byte)_acknowledgedSequenceCount;
segment.ControlFlags |= BTPControlFlags.Acknowledge;
headerLength += 1;
}
}
So now, when it’s sent a message, it would check if there were any unacknowledged messages and include it.
This worked a charm and the matter.js no longer complained! My PASE calculations didn’t work on the C# side, but at least the BTP session seemed to work.
Next step is adding more logging to the matter.js code to see if I can’t get a handle on this PBKDF stuff.
Digging into PASE
I started digging through matter.js and found some of the code in the protocol package. Specifically in the PaseServer.ts file. I added a few console.log statements to dump out some of the relevant data.
I wanted to compare the various steps of the PBKDF to ensure my calculations were the same. The hash was the first thing I wanted to compare.

I then restarted the app
You get a nice little indicator that it’s compiling the changes. As I’ve changed the PaseServer.ts file, it recompiled the protocol package.

Once it started, I kicked off my console app. After the PASE session started, the matter.js app spat this out:

and my .Net code spat this out:

Amazingly, these appears to be an exact match!!
So far, so good! This meant I had the right context hash. Being able to see the value on both sides was clearly how I was going to trouble shoot this!
I added logging to dump the computed values of V and Z.
The matter.js reported this:

and my code

Again, an exact match!
This left the Transcript hash and the HKDF functions. The transcript on the matter.js side looked like this:
200000000000000059236165c055c0ffbf6234d1ba53c729fd5f8b0b715343141b792afb2557dc0200000000000000000000000000000000410000000000000004886e2f97ace46e55ba9dd7242579f2993b64e16ef3dcab95afd497333d8fa12f5ff355163e43ce224e0b0e65ff02ac8e5c7be09419c785e0ca547d55a12e2d20410000000000000004d8bbd6c639c62937b04d997f38c3770719c629d7014d49a24b4f98baa1292b4907d60aa6bfade45008a636337f5168c64d9bd36034808cd564490b1e656edbe741000000000000000438415f8e2b7c6e4e770929a7a150524311b795543563a1e39385b1ee462cdea8dac1d919470923fdd079edf95509c99706409f55d3cacd15e745bfcfd2e31afa410000000000000004beec3d7b032f4cb23b41b19da4b8b605677514f74b22096c96b172305f0c706fbd66add33fc53ceef179b40b2cd97f6dc1e07e2e04e6fb1b717e3eeed829f736410000000000000004ff6b8887d1de6e3ae3b651864661a347f6b09ff1dbb7dde2b9469c75d519894c792d3cce8ecbfc6a077d3ca48dfbc8f62cd9313b456df16f5d4a11e1b06faa7d41000000000000000476fa488e73f8c56293bb5285009fbe9a4b1fae10bca6784f81c85d57c66bd0dc5e9498d8ae122694c1230dec9bebdda61d90279fae046274dc21486d3d4f3e37200000000000000091bf838d1964c10d7042de777b82c400156193af5a65f4ddd01a70a88f3e4213
Unfortunately, my .Net Transcript data didn’t quite match
00-00-00-00-00-00-00-00-20-00-00-00-00-00-00-00-BF-62-34-D1-BA-53-C7-29-FD-5F-8B-0B-71-53-43-14-1B-79-2A-FB-25-57-DC-02-00-00-00-00-00-00-00-00-08-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-41-00-00-00-00-00-00-00-55-BA-9D-D7-24-25-79-F2-99-3B-64-E1-6E-F3-DC-AB-95-AF-D4-97-33-3D-8F-A1-2F-5F-F3-55-16-3E-43-CE-22-4E-0B-0E-65-FF-02-AC-8E-5C-7B-E0-94-19-C7-85-E0-CA-54-7D-55-A1-2E-2D-20-00-00-00-00-00-00-00-00-41-00-00-00-00-00-00-00-37-B0-4D-99-7F-38-C3-77-07-19-C6-29-D7-01-4D-49-A2-4B-4F-98-BA-A1-29-2B-49-07-D6-0A-A6-BF-AD-E4-50-08-A6-36-33-7F-51-68-C6-4D-9B-D3-60-34-80-8C-D5-64-49-0B-1E-65-6E-DB-E7-00-00-00-00-00-00-00-00-41-00-00-00-00-00-00-00-4E-77-09-29-A7-A1-50-52-43-11-B7-95-54-35-63-A1-E3-93-85-B1-EE-46-2C-DE-A8-DA-C1-D9-19-47-09-23-FD-D0-79-ED-F9-55-09-C9-97-06-40-9F-55-D3-CA-CD-15-E7-45-BF-CF-D2-E3-1A-FA-00-00-00-00-00-00-00-00-41-00-00-00-00-00-00-00-B2-3B-41-B1-9D-A4-B8-B6-05-67-75-14-F7-4B-22-09-6C-96-B1-72-30-5F-0C-70-6F-BD-66-AD-D3-3F-C5-3C-EE-F1-79-B4-0B-2C-D9-7F-6D-C1-E0-7E-2E-04-E6-FB-1B-71-7E-3E-EE-D8-29-F7-36-00-00-00-00-00-00-00-00-41-00-00-00-00-00-00-00-3A-E3-B6-51-86-46-61-A3-47-F6-B0-9F-F1-DB-B7-DD-E2-B9-46-9C-75-D5-19-89-4C-79-2D-3C-CE-8E-CB-FC-6A-07-7D-3C-A4-8D-FB-C8-F6-2C-D9-31-3B-45-6D-F1-6F-5D-4A-11-E1-B0-6F-AA-7D-00-00-00-00-00-00-00-00-41-00-00-00-00-00-00-00-62-93-BB-52-85-00-9F-BE-9A-4B-1F-AE-10-BC-A6-78-4F-81-C8-5D-57-C6-6B-D0-DC-5E-94-98-D8-AE-12-26-94-C1-23-0D-EC-9B-EB-DD-A6-1D-90-27-9F-AE-04-62-74-DC-21-48-6D-3D-4F-3E-37-00-00-00-00-00-00-00-00-20-00-00-00-00-00-00-00-70-42-DE-77-7B-82-C4-00-15-61-93-AF-5A-65-F4-DD-D0-1A-70-A8-8F-3E-42-13
Looking closely, it seemed like the first 16 bytes were different.
I looked at my C# code again and immediately face palmed.

I was writing the length *over* the data itself, rather than into the lengthBytes array. Bloody hell.
Fixing the code and running it all again yield:
2000000000000000b5c4c8fe3695ccac03a88eca955204dbe28ed956540487fb4915c78fff89981e00000000000000000000000000000000410000000000000004886e2f97ace46e55ba9dd7242579f2993b64e16ef3dcab95afd497333d8fa12f5ff355163e43ce224e0b0e65ff02ac8e5c7be09419c785e0ca547d55a12e2d20410000000000000004d8bbd6c639c62937b04d997f38c3770719c629d7014d49a24b4f98baa1292b4907d60aa6bfade45008a636337f5168c64d9bd36034808cd564490b1e656edbe74100000000000000046c155ea88fa9a176c0eec686be4dcf5768a8b71f7eee2297dc2df50a0f43ebf70308f2968e80f6f858ce37502a027de6714d844b4f84077fbbeee397e1e94d47410000000000000004dfd36c111d686f864a990e736bef22b5f529a697d711ae09f7b30936ddd95a61885601af956d6ca244572ef12ef292fe5a68cd2068ee472113f5f938b1148b19410000000000000004a5bab89e7fcef02baf4ea52e9c166cd0035ec35b15a4f799e323af253d72140e50d9ac4e4a8bd00ba12b7424cbde18d46c8974690dbc7dc110973cf8efd843ab410000000000000004b44de57e4d9adff2bd8ee3a5ae56eb13301baa2fd402631dbfd0d561627bb8a49cd1cc435a003086f52b550ba5e19b08420a5fb93a478a8b64ee9b1d974c40a620000000000000007a62f140305b5a984655c6781e3b05eb2e0f830747e8f264ae4bb1eb5d1f3404
and comparing to my .Net code
20-00-00-00-00-00-00-00-B5-C4-C8-FE-36-95-CC-AC-03-A8-8E-CA-95-52-04-DB-E2-8E-D9-56-54-04-87-FB-49-15-C7-8F-FF-89-98-1E-08-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-41-00-00-00-00-00-00-00-04-88-6E-2F-97-AC-E4-6E-55-BA-9D-D7-24-25-79-F2-99-3B-64-E1-6E-F3-DC-AB-95-AF-D4-97-33-3D-8F-A1-2F-5F-F3-55-16-3E-43-CE-22-4E-0B-0E-65-FF-02-AC-8E-5C-7B-E0-94-19-C7-85-E0-CA-54-7D-55-A1-2E-2D-20-41-00-00-00-00-00-00-00-04-D8-BB-D6-C6-39-C6-29-37-B0-4D-99-7F-38-C3-77-07-19-C6-29-D7-01-4D-49-A2-4B-4F-98-BA-A1-29-2B-49-07-D6-0A-A6-BF-AD-E4-50-08-A6-36-33-7F-51-68-C6-4D-9B-D3-60-34-80-8C-D5-64-49-0B-1E-65-6E-DB-E7-41-00-00-00-00-00-00-00-04-6C-15-5E-A8-8F-A9-A1-76-C0-EE-C6-86-BE-4D-CF-57-68-A8-B7-1F-7E-EE-22-97-DC-2D-F5-0A-0F-43-EB-F7-03-08-F2-96-8E-80-F6-F8-58-CE-37-50-2A-02-7D-E6-71-4D-84-4B-4F-84-07-7F-BB-EE-E3-97-E1-E9-4D-47-41-00-00-00-00-00-00-00-04-DF-D3-6C-11-1D-68-6F-86-4A-99-0E-73-6B-EF-22-B5-F5-29-A6-97-D7-11-AE-09-F7-B3-09-36-DD-D9-5A-61-88-56-01-AF-95-6D-6C-A2-44-57-2E-F1-2E-F2-92-FE-5A-68-CD-20-68-EE-47-21-13-F5-F9-38-B1-14-8B-19-41-00-00-00-00-00-00-00-04-A5-BA-B8-9E-7F-CE-F0-2B-AF-4E-A5-2E-9C-16-6C-D0-03-5E-C3-5B-15-A4-F7-99-E3-23-AF-25-3D-72-14-0E-50-D9-AC-4E-4A-8B-D0-0B-A1-2B-74-24-CB-DE-18-D4-6C-89-74-69-0D-BC-7D-C1-10-97-3C-F8-EF-D8-43-AB-41-00-00-00-00-00-00-00-04-B4-4D-E5-7E-4D-9A-DF-F2-BD-8E-E3-A5-AE-56-EB-13-30-1B-AA-2F-D4-02-63-1D-BF-D0-D5-61-62-7B-B8-A4-9C-D1-CC-43-5A-00-30-86-F5-2B-55-0B-A5-E1-9B-08-42-0A-5F-B9-3A-47-8A-8B-64-EE-9B-1D-97-4C-40-A6-20-00-00-00-00-00-00-00-7A-62-F1-40-30-5B-5A-98-46-55-C6-78-1E-3B-05-EB-2E-0F-83-07-47-E8-F2-64-AE-4B-B1-EB-5D-1F-34-04
There appeared to be one difference; a 0x08 instead of a 0x00. I immediately knew that that was.
AddToContext(TTwriter, BitConverter.GetBytes((ulong)0));
The second pair of bytes in the Transcript should be zero, as specified here

I was writing the length of 8 bytes (the ulong) instead of zero. A quick tweak to the C# and I tried again.
That change did it!
My KcAB, hAY and hYB values suddenly matched! From matter.js
KcAB: cdb9f5e8b8aa35a887a4b2f28c462f0a4a56457e0114080f7b161936b4d56d52
hAY: 1bf6eb741efab9df4c158faf34e6b73bc13b9b1f35a0eceb73fcb3c01175ae21
hBX: 9479fe7c66d2ffba0a063ed5c31b6344afa783ce7298405f0d77e2781f75f9cc
and from .Net
KcAB: CDB9F5E8B8AA35A887A4B2F28C462F0A4A56457E0114080F7B161936B4D56D52
hAY: 1BF6EB741EFAB9DF4C158FAF34E6B73BC13B9B1F35A0ECEB73FCB3C01175AE21
hBX: 9479FE7C66D2FFBA0A063ED5C31B6344AFA783CE7298405F0D77E2781F75F9CC
Huzzah!
My cryptographic code for the PASE was almost complete. Last step was the final verification!
Pake3 – Finally!!!
With my hAY value finally matching the expected value, I needed to send a Pake3 message
var pake3 = new MatterTLV();
pake3.AddStructure();
pake3.Add1OctetString(1, hAY);
pake3.EndContainer();
var pake3MessagePayload = new MessagePayload(pake3);
The matter.js reported successful receipt of my Pake3.

I now had enough to generate the encryption keys for the session.
Summary
It’s been a long road, but I finally got my C# code working correctly and generating the right PASE payloads. This is the first milestone in the commissioning flow. Still a mountain of stuff to figure out, but I’m feeling more confident now.
Using the matter.js for debugging has been a game changer. It even helped identify an issue with my BTP code, so that’s positive.
With the PASE steps complete, I can now establish a secure session with the device. The next step, I believe, is connecting to the appropriate network WiFi or Thread. That’s for the next post!

Leave a comment