From 7f38e03840ea23b76c9180b3d410983af6b2a840 Mon Sep 17 00:00:00 2001
From: jdomnitz <380352+jdomnitz@users.noreply.github.com>
Date: Mon, 13 Jan 2025 00:53:04 -0500
Subject: [PATCH] Add support for enhanced commissioning
---
MatterDotNet/Entities/Node.cs | 35 ++++++++++++---
.../CommissioningPayload.cs | 17 +++++++
MatterDotNet/Protocol/Cryptography/Crypto.cs | 44 +++++++++----------
.../Protocol/Cryptography/SPAKE2Plus.cs | 14 ++++++
4 files changed, 82 insertions(+), 28 deletions(-)
diff --git a/MatterDotNet/Entities/Node.cs b/MatterDotNet/Entities/Node.cs
index 61f1040..edcd7fb 100644
--- a/MatterDotNet/Entities/Node.cs
+++ b/MatterDotNet/Entities/Node.cs
@@ -14,9 +14,11 @@
using MatterDotNet.OperationalDiscovery;
using MatterDotNet.PKI;
using MatterDotNet.Protocol.Connection;
+using MatterDotNet.Protocol.Cryptography;
using MatterDotNet.Protocol.Sessions;
using MatterDotNet.Protocol.Subprotocols;
using System.Net;
+using System.Security.Cryptography;
using System.Text;
namespace MatterDotNet.Entities
@@ -57,22 +59,45 @@ private Node(ODNode connection, Fabric fabric, OperationalCertificate noc)
///
public async Task GetCASESession()
{
- if (connection.IP6Address != null)
+ if (connection.IP4Address != null)
{
- using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.IP6Address!, connection.Port), true))
+ using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.IP4Address!, connection.Port), true))
return await GetCASESession(session);
}
- else if(connection.IP4Address != null)
+ else if (connection.IP6Address != null)
{
- using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.IP4Address!, connection.Port), true))
+ using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.IP6Address!, connection.Port), true))
return await GetCASESession(session);
}
else
{
- using (SessionContext session = SessionManager.GetUnsecureSession(new BLEEndPoint(connection.BTAddress), true))
+ using (SessionContext session = SessionManager.GetUnsecureSession(new BLEEndPoint(connection.BTAddress!), true))
return await GetCASESession(session);
}
}
+
+ ///
+ /// Open a commissioning window with a new randomly generateed PIN Code
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task OpenEnhancedCommissioning(SecureSession session, ushort timeoutSec = 300)
+ {
+ if (!root.HasCluster())
+ throw new PlatformNotSupportedException("Admin commissioning cluster not found");
+ uint passcode = Crypto.GeneratePasscode();
+ ushort discriminator = (ushort)(Random.Shared.Next() & 0xFFF);
+ string pin = CommissioningPayload.GeneratePIN(discriminator, passcode);
+ SPAKE2Plus spake = new SPAKE2Plus();
+ AdministratorCommissioningCluster com = root.GetCluster();
+ byte[] salt = RandomNumberGenerator.GetBytes(32);
+ if (!await com.OpenCommissioningWindow(session, 10000, timeoutSec, spake.CommissioneePakeInput(passcode, 10000, salt), discriminator, 10000, salt))
+ return null;
+ return pin;
+ }
+
///
/// Get a secure session for the node
///
diff --git a/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs b/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs
index 01508c8..7dc8a11 100644
--- a/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs
+++ b/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs
@@ -196,6 +196,23 @@ public static CommissioningPayload FromPIN(string pin)
return ret;
}
+ ///
+ /// Generate a pairing code from a 12-bit discriminator and passcode
+ ///
+ ///
+ ///
+ ///
+ public static string GeneratePIN(ushort descriminator, uint passcode)
+ {
+ descriminator >>= 8;
+ byte b1 = (byte)((descriminator & 0xF) >> 2);
+ ushort group1 = (ushort)((descriminator & 0x3) << 14);
+ group1 |= (ushort)(passcode & 0x3FFF);
+ uint group2 = passcode >> 14;
+ string ret = string.Concat(b1, group1.ToString("00000"), group2.ToString("0000"));
+ return ret.Substring(0, 4) + '-' + ret.Substring(4, 3) + '-' + ret.Substring(7) + Checksum.GenerateVerhoeff(ret);
+ }
+
private static byte[] Decode(string str)
{
List data = new List();
diff --git a/MatterDotNet/Protocol/Cryptography/Crypto.cs b/MatterDotNet/Protocol/Cryptography/Crypto.cs
index a6011ab..af0130a 100644
--- a/MatterDotNet/Protocol/Cryptography/Crypto.cs
+++ b/MatterDotNet/Protocol/Cryptography/Crypto.cs
@@ -13,6 +13,7 @@
using MatterDotNet.Security;
using System.Buffers.Binary;
using System.Security.Cryptography;
+using System.Text;
namespace MatterDotNet.Protocol.Cryptography
{
@@ -214,29 +215,26 @@ public static (byte[] Public, byte[] Private) GenerateKeypair()
return (pub, p.D!);
}
- //public static byte[] Sign(byte[] privateKey, byte[] message)
- //{
- // ArgumentNullException.ThrowIfNull(privateKey, nameof(privateKey));
- // ArgumentNullException.ThrowIfNull(message, nameof(message));
- // ECParameters ecp = new ECParameters();
- // ecp.Curve = ECCurve.NamedCurves.nistP256;
- // ecp.D = privateKey;
- // ECDsa ec = ECDsa.Create(ecp);
- // return ec.SignData(message, HashAlgorithmName.SHA256);
- //}
-
- //public static bool Verify(byte[] publicKey, byte[] message, byte[] signature)
- //{
- // ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey));
- // ArgumentNullException.ThrowIfNull(message, nameof(message));
- // ArgumentNullException.ThrowIfNull(signature, nameof(signature));
- // ECParameters ecp = new ECParameters();
- // ecp.Curve = ECCurve.NamedCurves.nistP256;
- // ecp.Q = new BigIntegerPoint(publicKey).ToECPoint();
- // ecp.Validate();
- // ECDsa ec = ECDsa.Create(ecp);
- // return ec.VerifyData(message, signature, HashAlgorithmName.SHA256);
- //}
+ public static uint GeneratePasscode()
+ {
+ StringBuilder pin = new StringBuilder(10);
+ for (int i = 0; i < 8; i++)
+ pin.Append((char)Random.Shared.Next(48, 57));
+ string ret = pin.ToString();
+ bool allSame = true;
+ char check = ret[0];
+ foreach (char c in ret)
+ {
+ if (check != c)
+ {
+ allSame = false;
+ break;
+ }
+ }
+ if (allSame || ret == "12345678" || ret == "87654321")
+ return GeneratePasscode();
+ return uint.Parse(ret);
+ }
public static byte[] ECDH(byte[] myPrivateKey, byte[] theirPublicKey)
{
diff --git a/MatterDotNet/Protocol/Cryptography/SPAKE2Plus.cs b/MatterDotNet/Protocol/Cryptography/SPAKE2Plus.cs
index 2e86a90..992461c 100644
--- a/MatterDotNet/Protocol/Cryptography/SPAKE2Plus.cs
+++ b/MatterDotNet/Protocol/Cryptography/SPAKE2Plus.cs
@@ -12,6 +12,7 @@
using MatterDotNet.Messages.PASE;
using MatterDotNet.Protocol.Payloads;
+using MatterDotNet.Security;
using System.Buffers.Binary;
using System.Numerics;
using System.Security.Cryptography;
@@ -68,6 +69,19 @@ public BigIntegerPoint PAKEValues_Responder(uint passcode, int iterations, byte[
return Y;
}
+ public byte[] CommissioneePakeInput(uint passcode, int iterations, byte[] salt)
+ {
+ byte[] pinBytes = new byte[4];
+ BinaryPrimitives.WriteUInt32LittleEndian(pinBytes, passcode);
+ byte[] w = Crypto.PBKDF(pinBytes, salt, iterations, Crypto.W_SIZE_BITS * 2);
+ BigInteger w0s = new BigInteger(w.AsSpan().Slice(0, Crypto.W_SIZE_BYTES), true, true);
+ BigInteger w1s = new BigInteger(w.AsSpan().Slice(Crypto.W_SIZE_BYTES, Crypto.W_SIZE_BYTES), true, true);
+ w0 = w0s % SecP256.n;
+ w1 = w1s % SecP256.n;
+ L = SecP256.Multiply(w1, SecP256.GeneratorP);
+ return SpanUtil.Combine(w0.ToByteArray(true, true), L.ToBytes(false));
+ }
+
public (BigIntegerPoint Z, BigIntegerPoint V) InitiatorValidate(BigIntegerPoint pB)
{
if (!SecP256.IsOnCurve(pB))