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!

Be sure to check out my YouTube channel.

2 responses

  1. […] hope this code will now reveal some details about my why my PASE sessions don’t […]

  2. […] my previous post, I got as far as the final PASE message, but no matter (!) what I did, the response was just never […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.