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))