Skip to content

Commit

Permalink
Merge pull request #26 from mfucci/integration-test
Browse files Browse the repository at this point in the history
Add an integration test and a bare-bone Matter controller
  • Loading branch information
javierbarellano4v authored Sep 30, 2022
2 parents 9e52d18 + 403eae1 commit 4c3e15b
Show file tree
Hide file tree
Showing 91 changed files with 2,582 additions and 972 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ For instance, on a Raspberry Pi, this will turn on / off the red LED:
matter -on "echo 255 > /sys/class/leds/led1/brightness" -off "echo 0 > /sys/class/leds/led1/brightness"
```

**Experimental**

```bash
matter-controller -ip [IP address of device to commission]
```

This will commission a Matter device (for debugging purpose only for now).


## Modifying the server behavior

Main.ts defines the server behavior. You can add / remove clusters, change default parameters, etc...
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"main": "build/Main.js",
"types": "build/Main.d.ts",
"bin": {
"matter": "./build/Main.js"
"matter": "./build/Main.js",
"matter-controller": "./build/Controller.js"
}
}
35 changes: 35 additions & 0 deletions src/Controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env node

/**
* @license
* Copyright 2022 Marco Fucci di Napoli ([email protected])
* SPDX-License-Identifier: Apache-2.0
*/

import { MatterClient } from "./matter/MatterClient";
import { MdnsMatterScanner } from "./mdns/MdnsMatterScanner";
import { UdpInterface } from "./net/MatterUdpInterface";
import { Network } from "./net/Network";
import { NetworkNode } from "./net/node/NetworkNode";
import { getIntParameter, getParameter } from "./util/CommandLine";
import { singleton } from "./util/Singleton";

Network.get = singleton(() => new NetworkNode());

class Main {
async start() {
const ip = getParameter("ip");
if (ip === undefined) throw new Error("Please specify the IP of the device to commission with -ip");
const port = getIntParameter("port") ?? 5540;
const discriminator = getIntParameter("discriminator") ?? 3840;
const setupPin = getIntParameter("pin") ?? 20202021;
const client = await MatterClient.create(await MdnsMatterScanner.create(), await UdpInterface.create(5540));
try {
await client.commission(ip, port, discriminator, setupPin);
} finally {
client.close();
}
}
}

new Main().start();
6 changes: 6 additions & 0 deletions src/Devices.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* @license
* Copyright 2022 Marco Fucci di Napoli ([email protected])
* SPDX-License-Identifier: Apache-2.0
*/

export const DEVICE = {
ROOT: { name: "MA-rootdevice", code: 0x0016 },
ON_OFF_LIGHT: { name: "MA-onofflight", code: 0x0100 },
Expand Down
49 changes: 21 additions & 28 deletions src/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,25 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { MatterServer, Protocol } from "./server/MatterServer";
import { UdpChannel } from "./channel/UdpChannel";
import { SecureChannelHandler } from "./session/secure/SecureChannelHandler";
import { PasePairing } from "./session/secure/PasePairing";
import { MatterServer } from "./matter/MatterServer";
import { UdpInterface } from "./net/MatterUdpInterface";
import { SecureChannelProtocol } from "./session/secure/SecureChannelProtocol";
import { PaseServer } from "./session/secure/PaseServer";
import { Crypto } from "./crypto/Crypto";
import { CasePairing } from "./session/secure/CasePairing";
import { CaseServer } from "./session/secure/CaseServer";
import { InteractionProtocol } from "./interaction/InteractionProtocol";
import { Device } from "./interaction/model/Device";
import { Endpoint } from "./interaction/model/Endpoint";
import { BasicCluster } from "./interaction/cluster/BasicCluster";
import { BasicClusterServer } from "./interaction/cluster/BasicCluster";
import { GeneralCommissioningCluster } from "./interaction/cluster/GeneralCommissioningCluster";
import { OperationalCredentialsCluster } from "./interaction/cluster/OperationalCredentialsCluster";
import { OnOffCluster } from "./interaction/cluster/OnOffCluster";
import { execSync } from "child_process";
import { DEVICE } from "./Devices";

const commandArguments = process.argv.slice(2);

function getParameter(name: string) {
const markerIndex = commandArguments.indexOf(`-${name}`);
if (markerIndex === -1 || markerIndex + 1 === commandArguments.length) return undefined;
return commandArguments[markerIndex + 1];
}

function commandExecutor(scriptParamName: string) {
const script = getParameter(scriptParamName);
if (script === undefined) return undefined;
return () => console.log(`${scriptParamName}: ${execSync(script).toString().slice(0, -1)}`);
}
import { MdnsMatterBroadcaster } from "./mdns/MdnsMatterBroadcaster";
import { Network } from "./net/Network";
import { NetworkNode } from "./net/node/NetworkNode";
import { commandExecutor } from "./util/CommandLine";
import { singleton } from "./util/Singleton";

// From Chip-Test-DAC-FFF1-8000-0007-Key.der
const DevicePrivateKey = Buffer.from("727F1005CBA47ED7822A9D930943621617CFD3B79D9AF528B801ECF9F1992204", "hex");
Expand All @@ -48,6 +38,8 @@ const ProductIntermediateCertificate = Buffer.from("308201d43082017aa00302010202
// From DeviceAttestationCredsExample.cpp
const CertificateDeclaration = Buffer.from("3082021906092a864886f70d010702a082020a30820206020103310d300b06096086480165030402013082017106092a864886f70d010701a08201620482015e152400012501f1ff3602050080050180050280050380050480050580050680050780050880050980050a80050b80050c80050d80050e80050f80051080051180051280051380051480051580051680051780051880051980051a80051b80051c80051d80051e80051f80052080052180052280052380052480052580052680052780052880052980052a80052b80052c80052d80052e80052f80053080053180053280053380053480053580053680053780053880053980053a80053b80053c80053d80053e80053f80054080054180054280054380054480054580054680054780054880054980054a80054b80054c80054d80054e80054f80055080055180055280055380055480055580055680055780055880055980055a80055b80055c80055d80055e80055f80056080056180056280056380182403162c04135a494732303134325a423333303030332d32342405002406002507942624080018317d307b020103801462fa823359acfaa9963e1cfa140addf504f37160300b0609608648016503040201300a06082a8648ce3d04030204473045022024e5d1f47a7d7b0d206a26ef699b7c9757b72d469089de3192e678c745e7f60c022100f8aa2fa711fcb79b97e397ceda667bae464e2bd3ffdfc3cced7aa8ca5f4c1a7c", "hex");

Network.get = singleton(() => new NetworkNode());

class Main {
async start() {
const deviceName = "Matter test device";
Expand All @@ -57,15 +49,16 @@ class Main {
const productName = "Matter test device";
const productId = 0X8001;
const discriminator = 3840;
(await MatterServer.create(deviceName, deviceType, vendorId, productId, discriminator))
.addChannel(new UdpChannel(5540))
.addProtocolHandler(Protocol.SECURE_CHANNEL, new SecureChannelHandler(
new PasePairing(20202021, { iteration: 1000, salt: Crypto.getRandomData(32) }),
new CasePairing(),
(new MatterServer(deviceName, deviceType, vendorId, productId, discriminator))
.addNetInterface(await UdpInterface.create(5540))
.addBroadcaster(await MdnsMatterBroadcaster.create())
.addProtocol(new SecureChannelProtocol(
new PaseServer(20202021, { iteration: 1000, salt: Crypto.getRandomData(32) }),
new CaseServer(),
))
.addProtocolHandler(Protocol.INTERACTION_MODEL, new InteractionProtocol(new Device([
.addProtocol(new InteractionProtocol(new Device([
new Endpoint(0x00, DEVICE.ROOT, [
BasicCluster.Builder({ vendorName, vendorId, productName, productId }),
BasicClusterServer.Builder({ vendorName, vendorId, productName, productId }),
GeneralCommissioningCluster.Builder(),
OperationalCredentialsCluster.Builder({devicePrivateKey: DevicePrivateKey, deviceCertificate: DeviceCertificate, deviceIntermediateCertificate: ProductIntermediateCertificate, certificateDeclaration: CertificateDeclaration}),
]),
Expand Down
46 changes: 0 additions & 46 deletions src/channel/UdpChannel.ts

This file was deleted.

118 changes: 98 additions & 20 deletions src/codec/DerCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,95 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { BEBufferReader } from "../util/BEBufferReader";

export const OBJECT_ID_KEY = "_objectId";
export const END_MARKER = {};
export const TAG_ID_KEY = "_tag";
export const BYTES_KEY = "_bytes";
export const ELEMENTS_KEY = "_elements";
export const BITS_PADDING = "_padding";

const enum DerType {
Boolean = 0x01,
UnsignedInt = 0x02,
BitString = 0x03,
OctetString = 0x04,
ObjectIdentifier = 0x06,
UTF8String = 0x0C,
Sequence = 0x10,
Set = 0x11,
UTF8String = 0x0C,
EndMarker = 0xA0,
UtcDate = 0x17,
}

const CONSTRUCTED = 0x20;

const enum DerClass {
Universal = 0x00,
Application = 0x40,
ContextSpecific = 0x80,
Private = 0xC0,
}
export const ObjectId = (objectId: string) => ({ [TAG_ID_KEY]: DerType.ObjectIdentifier as number, [BYTES_KEY]: Buffer.from(objectId, "hex") });
export const DerObject = (objectId: string, content: any = {}) => ({ [OBJECT_ID_KEY]: ObjectId(objectId), ...content });
export const BitBuffer = (data: Buffer, padding: number = 0) => ({ [TAG_ID_KEY]: DerType.BitString as number, [BYTES_KEY]: data, [BITS_PADDING]: padding });
export const ContextTagged = (tagId: number, value?: any) => ({ [TAG_ID_KEY]: tagId | DerClass.ContextSpecific | CONSTRUCTED, [BYTES_KEY]: value === undefined ? Buffer.alloc(0) : DerCodec.encode(value) });
export const ContextTaggedBytes = (tagId: number, value: Buffer) => ({ [TAG_ID_KEY]: tagId | DerClass.ContextSpecific, [BYTES_KEY]: value });


export type DerNode = {
[TAG_ID_KEY]: number,
[BYTES_KEY]: Buffer,
[ELEMENTS_KEY]?: DerNode[],
[BITS_PADDING]?: number,
}

export class DerCodec {
static encode(value: any): Buffer {
if (Array.isArray(value)) {
return this.encodeArray(value);
} else if (Buffer.isBuffer(value)) {
return this.encodeBitString(value);
} else if (value === END_MARKER) {
return this.encodeEndMarker();
return this.encodeOctetString(value);
} else if (value instanceof Date) {
return this.encodeDate(value);
} else if (typeof value === "object" && value[TAG_ID_KEY] !== undefined) {
return this.encodeAnsi1(value[TAG_ID_KEY], (value[BITS_PADDING] === undefined) ? value[BYTES_KEY] : Buffer.concat([Buffer.alloc(1, value[BITS_PADDING]), value[BYTES_KEY]]));
} else if (typeof value === "object") {
return this.encodeObject(value);
} else if (typeof value === "string") {
}else if (typeof value === "string") {
return this.encodeString(value);
} else if (typeof value === "number") {
return this.encodeUnsignedInt(value);
} else if (typeof value === "boolean") {
return this.encodeBoolean(value);
} else if (value === undefined) {
return Buffer.alloc(0);
} else {
throw new Error(`Unsupported type ${typeof value}`);
}
}

private static encodeArray(array: Array<any>) {
return this.encodeAnsi1(DerType.Set, Buffer.concat(array.map(element => this.encode(element))), true);
private static encodeDate(date: Date) {
return this.encodeAnsi1(DerType.UtcDate, Buffer.from(date.toISOString().replace(/[-:\.T]/g, "").slice(2, 14) + "Z"));
}

private static encodeBitString(value: Buffer) {
return this.encodeAnsi1(DerType.BitString, Buffer.concat([Buffer.alloc(1), value]));
private static encodeBoolean(bool: boolean) {
return this.encodeAnsi1(DerType.Boolean, Buffer.alloc(1, bool ? 0xFF : 0));
}

private static encodeEndMarker() {
return this.encodeAnsi1(DerType.EndMarker, Buffer.alloc(0));
private static encodeArray(array: Array<any>) {
return this.encodeAnsi1(DerType.Set | CONSTRUCTED, Buffer.concat(array.map(element => this.encode(element))));
}

private static encodeOctetString(value: Buffer) {
return this.encodeAnsi1(DerType.OctetString, value);
}

private static encodeObject(object: any) {
const attributes = new Array<Buffer>();
const objectId = object[OBJECT_ID_KEY];
if (objectId !== undefined) {
return this.encodeAnsi1(DerType.ObjectIdentifier, objectId);
}
for (var key in object) {
attributes.push(this.encode(object[key]));
}
return this.encodeAnsi1(DerType.Sequence, Buffer.concat(attributes), true);
return this.encodeAnsi1(DerType.Sequence | CONSTRUCTED, Buffer.concat(attributes));
}

private static encodeString(value: string) {
Expand Down Expand Up @@ -95,9 +129,53 @@ export class DerCodec {
return buffer.slice(start);
}

private static encodeAnsi1(tag: number, data: Buffer, constructed: boolean = false) {
private static encodeAnsi1(tag: number, data: Buffer) {
const tagBuffer = Buffer.alloc(1);
tagBuffer.writeUInt8(tag | (constructed ? 0x20 : 0));
tagBuffer.writeUInt8(tag);
return Buffer.concat([tagBuffer, this.encodeLengthBytes(data.length), data]);
}

static decode(data: Buffer): DerNode {
return this.decodeRec(new BEBufferReader(data));
}

private static decodeRec(reader: BEBufferReader): DerNode {
const { tag, bytes } = this.decodeAnsi1(reader);
if (tag === DerType.BitString) return { [TAG_ID_KEY]: tag, [BYTES_KEY]: bytes.slice(1), [BITS_PADDING]: bytes[0] };
if ((tag & CONSTRUCTED) === 0) return { [TAG_ID_KEY]: tag, [BYTES_KEY]: bytes };
const elementsReader = new BEBufferReader(bytes);
const elements = [];
while (elementsReader.getRemainingBytesCount() > 0) {
elements.push(this.decodeRec(elementsReader));
}
return { [TAG_ID_KEY]: tag, [BYTES_KEY]: bytes, [ELEMENTS_KEY]: elements };
}

private static decodeAnsi1(reader: BEBufferReader): { tag: number, bytes: Buffer } {
const tag = reader.readUInt8();
let length = reader.readUInt8();
if ((length & 0x80) !== 0) {
let lengthLength = length & 0x7F;
length = 0;
while (lengthLength > 0) {
length = (length << 8) + reader.readUInt8();
lengthLength--;
}
}
const bytes = reader.readBytes(length);
return { tag, bytes };
}
}

export const PublicKeyEcPrime256v1_X962 = (key: Buffer) => ({ type: { algorithm: ObjectId("2A8648CE3D0201") /* EC Public Key */, curve: ObjectId("2A8648CE3D030107") /* Curve P256_V1 */ }, bytes: BitBuffer(key) });
export const EcdsaWithSHA256_X962 = DerObject("2A8648CE3D040302");
export const OrganisationName_X520 = (name: string) => [ DerObject("55040A", { name }) ];
export const SubjectKeyIdentifier_X509 = (identifier: Buffer) => DerObject("551d0e", { value: DerCodec.encode(identifier) });
export const AuthorityKeyIdentifier_X509 = (identifier: Buffer) => DerObject("551d23", { value: DerCodec.encode({ id: ContextTaggedBytes(0, identifier) }) });
export const BasicConstraints_X509 = (constraints: any) => DerObject("551d13", { critical: true, value: DerCodec.encode(constraints) })
export const ExtendedKeyUsage_X509 = ({clientAuth, serverAuth}: {clientAuth: boolean, serverAuth: boolean}) => DerObject("551d25", { critical: true, value: DerCodec.encode({
client: clientAuth ? ObjectId("2b06010505070302") : undefined,
server: serverAuth ? ObjectId("2b06010505070301") : undefined,
})});
export const KeyUsage_Signature_X509 = DerObject("551d0f", { critical: true, value: DerCodec.encode(BitBuffer(Buffer.from([ 1 << 7]), 7)) });
export const KeyUsage_Signature_ContentCommited_X509 = DerObject("551d0f", { critical: true, value: DerCodec.encode(BitBuffer(Buffer.from([ 0x03 << 1]), 1)) });
Loading

0 comments on commit 4c3e15b

Please sign in to comment.