I’m trying to build a Matter Controller using .Net. 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 successfully exchanged sent and received the first pair of PASE messages. This was a significant milestone as it represented a large chunk of the BTP and MessageFrame.
The next message (Pake1) requires some cryptography, namely in the form of this method:

This stuff isn’t my strong suit, but I’m going to have a bash at turning that into some C#.
Unfortunately, there doesn’t appear to be a nuget library for this, so I have to roll my own. Let’s start with what we need to pass in; a passcode and the salt, interations values from the PBKDF response. Then we do some magic with this.
The passcode needed by my ESP32-H2 is the hardcoded 20202021 value.
CryptographyMethods.Crypto_PAKEValues_Initiator(20202021, iterations, salt);
First step, is turning the passcode into some bytes.
var passcodeBytes = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(passcodeBytes, passcode);
That was easy. There seems to be a call to another function called Crypto_PBKDF. As it happens, I dabbled with this stuff before, when I was trying to make some .Net HomeKit stuff.
The method is defined here

Thankfully, .Net Core has an implementation of this here – https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes.pbkdf2
I ended up with this:
var pbkdf = Rfc2898DeriveBytes.Pbkdf2(passcodeBytes, salt, iterations, HashAlgorithmName.SHA256, 32);
I’m not sure if the algorithm is right, but it’s a start.
At this point, I turned to DeepSeek for help. I just don’t know how to read that pseudo code. DeepSeek indicated that I take the pbkdf output and split it into two buffers w0s and w1s. After a little tinkering, it looked like this:
var GROUP_SIZE_BYTES = 32;
var CRYPTO_W_SIZE_BYTES = GROUP_SIZE_BYTES + 8;
var passcodeBytes = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(passcodeBytes, passcode);
var pbkdf = Rfc2898DeriveBytes.Pbkdf2(passcodeBytes, salt, iterations, HashAlgorithmName.SHA256, 2 * CRYPTO_W_SIZE_BYTES);
var w0s = pbkdf.AsSpan().Slice(0, CRYPTO_W_SIZE_BYTES);
var w1s = pbkdf.AsSpan().Slice(CRYPTO_W_SIZE_BYTES, CRYPTO_W_SIZE_BYTES);
Next step was “w0s mod p”.
DeepSeek suggested this value for p as it’s from the NIST P-256 curve (whatever the hell that is!)
0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
Next problem is how the hell do you perform modulus on byte arrays?? A quick google yielded the existence of the BigInteger in .Net. Switching to that yielded this:
var w0s = new BigInteger(pbkdf.AsSpan().Slice(0, CRYPTO_W_SIZE_BYTES));
var w1s = new BigInteger(pbkdf.AsSpan().Slice(CRYPTO_W_SIZE_BYTES, CRYPTO_W_SIZE_BYTES));
var p = new BigInteger(Encoding.ASCII.GetBytes("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF"));
var w0 = w0s % p;
var w1 = w1s % p;
Thanks DeepSeek.
Getting pA
With my w0 and w1 values, I now need to compute pA.

That’s even more difficult to decipher than the previous one!
DeepSeek gave me this forumlae:
pA = w0 * N + w1 * M
N and M are defined in the specification

And I defined them like this:
var M = new BigInteger(Convert.FromHexString("02886E2F97ACE46E55BA9DD7242579F2993B64E16EF3DCAB95AFD497333D8FA12F"));
var N = new BigInteger(Convert.FromHexString("03D8BBD6C639C62937B04D997F38C3770719C629D7014D49A24B4F98BAA1292B49"));
To work out pA, I did this:
var pA = BigInteger.Add(BigInteger.Multiply(w0, N), BigInteger.Multiply(w1, M));
I then create a Pake1 payload
var Pake1 = new MatterTLV();
Pake1.AddStructure();
var pA = CryptographyMethods.Crypto_PAKEValues_Initiator(20202021, iterations, salt);
Pake1.Add4OctetString(1, pA.ToByteArray());
Unsurprisingly, this didn’t work 🤣

RFC – Going old skool
I decided to have a go at reading the SPAKE2+ RFC – https://datatracker.ietf.org/doc/rfc9383/
We all know that AI is great, but hardly infallible!
Section 3.3 drew my attention.
To begin, the Prover selects x uniformly at random from the integers in [0, p-1], computes
the public share shareP=X, and transmits it to the Verifier.
“shareP” seems to align with pA as it’s initially sent.
The RFC provides this
x <- [0, p-1]
X = x*P + w0*M
We have P, w0 and M. The only part is a random number called x.
Actually, we have little p, not big P. What the hell is P?
We fix a generator P of the (large) prime-order subgroup of G.
Clear as mud 🤣
As I dug around, I found a section in the Specification about Group

It pointed to https://secg.org/sec2-v2.pdf and I searched for prime256v1 as that is the curve defined.

It also defines p

and this matches the value DeepSeek recommended! Progress!!
I tweaked my code, but got the same error. After a small break, I noticed I’d made a mistake in the call to PBKDF method. I was using the wrong length. It is 2 * CRYPTO_W_SIZE_BITS. I was using 2 & CRYPTO_W_SIZE_BYTES
Crypto_PBKDF(passcode, salt, iterations, 2 * CRYPTO_W_SIZE_BITS)
Fixing that still resulted in the same error from the ESP32.
Time to cheat!
There are three ways to make a living in this business: be first, be smarter, or cheat. Well, I don’t cheat. And although I like to think we have some smart people here. It sure is a hell of a lot easier to just be first
Unlike the man in Margin Call (2011), I will cheat. To cheat, I’m just going to look at the Matter SDK itself. Specifically, the JavaScript implementation. https://github.com/project-chip/matter.js
static async computeW0W1({ iterations, salt }: PbkdfParameters, pin: number) {
const pinWriter = new DataWriter(Endian.Little);
pinWriter.writeUInt32(pin);
const ws = await Crypto.pbkdf2(pinWriter.toByteArray(), salt, iterations, CRYPTO_W_SIZE_BYTES * 2);
const w0 = mod(bytesToNumberBE(ws.slice(0, 40)), P256_CURVE.n);
const w1 = mod(bytesToNumberBE(ws.slice(40, 80)), P256_CURVE.n);
return { w0, w1 };
}
My code mostly follows this logic, but the P256_CURVE.n is used in the modulus. Why does the Matter does show the variable as p? I’m convinced that using cryptography is easy, but it’s made to look really difficult!
I also confirmed that the pin code I was using, 20202021, was correct, by pairing the ESP32-H2 with iOS.
Time to start lining up my code with the JS!
As I started lookign at the JavaScript, I also discovered that the are some scripts available as part of the connectedhomeip/scripts/tools/spake2p/spake2p.py at master · project-chip/connectedhomeip
This help shed some light. The SALT value being returned from my ESP32 is actually a string; SPAKE2P Key Salt. This means it’s very predicable! Using the python code, I checked my C# pbkdf2 output matched its output. So that’s good.
Going back to the MatterJS, the main difference in my implementation is the random number part:
const random = Crypto.getRandomBigInt(32, P256_CURVE.Fp.ORDER);
This code appears to limit the size of the random number generated.
My code just generated a random number:
var x = new BigInteger(RandomNumberGenerator.GetBytes(GROUP_SIZE_BYTES), true);
After reviewing I spotted something very odd in the outgoing message. It was only 72 bytes in length.

But the pA payload was 65….
and the header of the MessageFrame and MessagePayload is *at least* 15…..
then I felt sick.

I was sending the previous bloody message!!!!!
Well, this is different:

The ESP32 isn’t returning an error, but it’s not responding to my message at all!!! After a little while, it shows an error:

After a few tweaks to ensure my MessageFrames were right, I got this!!

Pake2 (OpCode 23) coming back!

Can’t believe how much time I wasted on this!!!!! It’s what I get for cheating?? 🤣
From the MatterJS, the next thing to do it process the Pake2 response like this:
const { Ke, hAY, hBX } = await spake2p.computeSecretAndVerifiersFromY(w1, X, Y);
I chipped away at this, using DeepSeek to guide me along, helping me convert the terms. It’s proving to be pretty useful

After a lot of to and fro, I got something compiling. That’s always a good sign.
Unsurprisingly, it didn’t work first time. Arrays were the right sizes etc., so that’s always a good start.

After another few hours, spread out across a few days, I wasn’t making any significant progress. I added a Unit Test to my project and tested some of the SPAKE2+ test vectors. I discovered that I needed to set the sign of my “BigInteger” to 1 to make the test pass. Still, not matter what I did, the hBX calculation never matched.
A change in approach
Insanity is doing the same thing over and over and expecting different results.
I’m operating a little in the blind here, since I can’t tell what is happening in the ESP32. I need to confirm some of my assumptions about the steps.
I had a go at upping the logging within the ESP32 code, but it didn’t help. It then occurred to me that perhaps the chip-tool could act as a device, but it can’t. Thankfully, it seems that the matter.js library *can*!
For the next step in this process, I’m going to try and get matter.js stood up and running as a device. I’ll then attempt to commission it and see if I can figure out what I’m missing!




Leave a comment