about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSandro <sandro.jaeckel@gmail.com>2024-03-01 13:51:19 +0100
committerGitHub <noreply@github.com>2024-03-01 13:51:19 +0100
commitb84bc4ea3aace98ce0084d8cfc0a1b0622eba845 (patch)
tree1919bf63b43e9f9ec5ed22e2bac87957d85c72e3
parentc25cdf9c485c7228f1090a0991bb5ba377ac1fff (diff)
parentc0846f900a71e9bde3bf292882506cbb7cc97528 (diff)
downloadnixlib-b84bc4ea3aace98ce0084d8cfc0a1b0622eba845.tar
nixlib-b84bc4ea3aace98ce0084d8cfc0a1b0622eba845.tar.gz
nixlib-b84bc4ea3aace98ce0084d8cfc0a1b0622eba845.tar.bz2
nixlib-b84bc4ea3aace98ce0084d8cfc0a1b0622eba845.tar.lz
nixlib-b84bc4ea3aace98ce0084d8cfc0a1b0622eba845.tar.xz
nixlib-b84bc4ea3aace98ce0084d8cfc0a1b0622eba845.tar.zst
nixlib-b84bc4ea3aace98ce0084d8cfc0a1b0622eba845.zip
Merge pull request #264087 from leonm1/matter-server-module
-rw-r--r--maintainers/maintainer-list.nix9
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md4
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/home-automation/matter-server.nix125
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/matter-server.nix45
-rw-r--r--pkgs/development/python-modules/python-matter-server/default.nix32
-rw-r--r--pkgs/development/python-modules/python-matter-server/link-paa-root-certs.patch126
8 files changed, 343 insertions, 0 deletions
diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix
index 5d7eeca7cfaa..fae671dc12b7 100644
--- a/maintainers/maintainer-list.nix
+++ b/maintainers/maintainer-list.nix
@@ -10842,6 +10842,15 @@
     githubId = 77865363;
     name = "Leonid Belyaev";
   };
+  leonm1 = {
+    github = "leonm1";
+    githubId = 32306579;
+    keys = [{
+      fingerprint = "C12D F14B DC9D 64E1 44C3  4D8A 755C DA4E 5923 416A";
+    }];
+    matrix = "@mattleon:matrix.org";
+    name = "Matt Leon";
+  };
   leshainc = {
     email = "leshainc@fomalhaut.me";
     github = "LeshaInc";
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index fd286a4e6e69..a6a09755c14d 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -78,6 +78,10 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [hebbot](https://github.com/haecker-felix/hebbot), a Matrix bot to generate "This Week in X" like blog posts. Available as [services.hebbot](#opt-services.hebbot.enable).
 
+- [Python Matter Server](https://github.com/home-assistant-libs/python-matter-server), a
+  Matter Controller Server exposing websocket connections for use with other services, notably Home Assistant.
+  Available as [services.matter-server](#opt-services.matter-server.enable)
+
 - [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).
 The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been marked deprecated and will be dropped after 24.05 due to lack of maintenance of the anki-sync-server softwares.
 
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 00ad2ddd51b8..627427262da6 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -585,6 +585,7 @@
   ./services/home-automation/govee2mqtt.nix
   ./services/home-automation/home-assistant.nix
   ./services/home-automation/homeassistant-satellite.nix
+  ./services/home-automation/matter-server.nix
   ./services/home-automation/zigbee2mqtt.nix
   ./services/home-automation/zwave-js.nix
   ./services/logging/SystemdJournal2Gelf.nix
diff --git a/nixos/modules/services/home-automation/matter-server.nix b/nixos/modules/services/home-automation/matter-server.nix
new file mode 100644
index 000000000000..864ef9e20083
--- /dev/null
+++ b/nixos/modules/services/home-automation/matter-server.nix
@@ -0,0 +1,125 @@
+{ lib
+, pkgs
+, config
+, ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.matter-server;
+  storageDir = "matter-server";
+  storagePath = "/var/lib/${storageDir}";
+  vendorId = "4939"; # home-assistant vendor ID
+in
+
+{
+  meta.maintainers = with lib.maintainers; [ leonm1 ];
+
+  options.services.matter-server = with types; {
+    enable = mkEnableOption (lib.mdDoc "Matter-server");
+
+    package = mkPackageOptionMD pkgs "python-matter-server" { };
+
+    port = mkOption {
+      type = types.port;
+      default = 5580;
+      description = "Port to expose the matter-server service on.";
+    };
+
+    logLevel = mkOption {
+      type = types.enum [ "critical" "error" "warning" "info" "debug" ];
+      default = "info";
+      description = "Verbosity of logs from the matter-server";
+    };
+
+    extraArgs = mkOption {
+      type = listOf str;
+      default = [];
+      description = ''
+        Extra arguments to pass to the matter-server executable.
+        See https://github.com/home-assistant-libs/python-matter-server?tab=readme-ov-file#running-the-development-server for options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.matter-server = {
+      after = [ "network-online.target" ];
+      before = [ "home-assistant.service" ];
+      wants = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      description = "Matter Server";
+      environment.HOME = storagePath;
+      serviceConfig = {
+        ExecStart = (concatStringsSep " " [
+          "${cfg.package}/bin/matter-server"
+          "--port" (toString cfg.port)
+          "--vendorid" vendorId
+          "--storage-path" storagePath
+          "--log-level" "${cfg.logLevel}"
+          "${escapeShellArgs cfg.extraArgs}"
+        ]);
+        # Start with a clean root filesystem, and allowlist what the container
+        # is permitted to access.
+        TemporaryFileSystem = "/";
+        # Allowlist /nix/store (to allow the binary to find its dependencies)
+        # and dbus.
+        ReadOnlyPaths = "/nix/store /run/dbus";
+        # Let systemd manage `/var/lib/matter-server` for us inside the
+        # ephemeral TemporaryFileSystem.
+        StateDirectory = storageDir;
+        # `python-matter-server` writes to /data even when a storage-path is
+        # specified. This bind-mount points /data at the systemd-managed
+        # /var/lib/matter-server, so all files get dropped into the state
+        # directory.
+        BindPaths = "${storagePath}:/data";
+
+        # Hardening bits
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        DynamicUser = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = concatStringsSep " " [
+          "~" # Blocklist
+          "@clock"
+          "@cpu-emulation"
+          "@debug"
+          "@module"
+          "@mount"
+          "@obsolete"
+          "@privileged"
+          "@raw-io"
+          "@reboot"
+          "@resources"
+          "@swap"
+        ];
+        UMask = "0077";
+      };
+    };
+  };
+}
+
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index e36d49678a89..9795023bcea9 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -512,6 +512,7 @@ in {
   mastodon = discoverTests (import ./web-apps/mastodon { inherit handleTestOn; });
   pixelfed = discoverTests (import ./web-apps/pixelfed { inherit handleTestOn; });
   mate = handleTest ./mate.nix {};
+  matter-server = handleTest ./matter-server.nix {};
   matomo = handleTest ./matomo.nix {};
   matrix-appservice-irc = handleTest ./matrix/appservice-irc.nix {};
   matrix-conduit = handleTest ./matrix/conduit.nix {};
diff --git a/nixos/tests/matter-server.nix b/nixos/tests/matter-server.nix
new file mode 100644
index 000000000000..c646e9840d19
--- /dev/null
+++ b/nixos/tests/matter-server.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} :
+
+let
+  chipVersion = pkgs.python311Packages.home-assistant-chip-core.version;
+in
+
+{
+  name = "matter-server";
+  meta.maintainers = with lib.maintainers; [ leonm1 ];
+
+  nodes = {
+    machine = { config, ... }: {
+      services.matter-server = {
+        enable = true;
+        port = 1234;
+      };
+    };
+  };
+
+  testScript = /* python */ ''
+    start_all()
+
+    machine.wait_for_unit("matter-server.service")
+    machine.wait_for_open_port(1234)
+
+    with subtest("Check websocket server initialized"):
+        output = machine.succeed("echo \"\" | ${pkgs.websocat}/bin/websocat ws://localhost:1234/ws")
+        machine.log(output)
+
+    assert '"sdk_version": "${chipVersion}"' in output, (
+      'CHIP version \"${chipVersion}\" not present in websocket message'
+    )
+
+    assert '"fabric_id": 1' in output, (
+      "fabric_id not propagated to server"
+    )
+
+    with subtest("Check storage directory is created"):
+        machine.succeed("ls /var/lib/matter-server/chip.json")
+
+    with subtest("Check systemd hardening"):
+        _, output = machine.execute("systemd-analyze security matter-server.service | grep -v '✓'")
+        machine.log(output)
+  '';
+})
diff --git a/pkgs/development/python-modules/python-matter-server/default.nix b/pkgs/development/python-modules/python-matter-server/default.nix
index 588f2042bd9b..31064f369379 100644
--- a/pkgs/development/python-modules/python-matter-server/default.nix
+++ b/pkgs/development/python-modules/python-matter-server/default.nix
@@ -2,6 +2,8 @@
 , buildPythonPackage
 , fetchFromGitHub
 , pythonOlder
+, stdenvNoCC
+, substituteAll
 
 # build
 , setuptools
@@ -28,6 +30,29 @@
 , pytestCheckHook
 }:
 
+let
+  paaCerts = stdenvNoCC.mkDerivation rec {
+    pname = "matter-server-paa-certificates";
+    version = "1.2.0.1";
+
+    src = fetchFromGitHub {
+      owner = "project-chip";
+      repo = "connectedhomeip";
+      rev = "refs/tags/v${version}";
+      hash = "sha256-p3P0n5oKRasYz386K2bhN3QVfN6oFndFIUWLEUWB0ss=";
+    };
+
+    installPhase = ''
+      runHook preInstall
+
+      mkdir -p $out
+      cp $src/credentials/development/paa-root-certs/* $out/
+
+      runHook postInstall
+    '';
+  };
+in
+
 buildPythonPackage rec {
   pname = "python-matter-server";
   version = "5.7.0b2";
@@ -42,6 +67,13 @@ buildPythonPackage rec {
     hash = "sha256-fMtvVizHeAzLdou0U1tqbmQATIBLK4w9I7EwMlzB8QA=";
   };
 
+  patches = [
+    (substituteAll {
+      src = ./link-paa-root-certs.patch;
+      paacerts = paaCerts;
+    })
+  ];
+
   postPatch = ''
     substituteInPlace pyproject.toml \
       --replace 'version = "0.0.0"' 'version = "${version}"' \
diff --git a/pkgs/development/python-modules/python-matter-server/link-paa-root-certs.patch b/pkgs/development/python-modules/python-matter-server/link-paa-root-certs.patch
new file mode 100644
index 000000000000..a788f69144b8
--- /dev/null
+++ b/pkgs/development/python-modules/python-matter-server/link-paa-root-certs.patch
@@ -0,0 +1,126 @@
+diff --git a/matter_server/server/const.py b/matter_server/server/const.py
+index b6cd839..f9f798f 100644
+--- a/matter_server/server/const.py
++++ b/matter_server/server/const.py
+@@ -5,14 +5,4 @@ from typing import Final
+ # The minimum schema version (of a client) the server can support
+ MIN_SCHEMA_VERSION = 5
+ 
+-# the paa-root-certs path is hardcoded in the sdk at this time
+-# and always uses the development subfolder
+-# regardless of anything you pass into instantiating the controller
+-# revisit this once matter 1.1 is released
+-PAA_ROOT_CERTS_DIR: Final[pathlib.Path] = (
+-    pathlib.Path(__file__)
+-    .parent.resolve()
+-    .parent.resolve()
+-    .parent.resolve()
+-    .joinpath("credentials/development/paa-root-certs")
+-)
++PAA_ROOT_CERTS_DIR: Final[pathlib.Path] = pathlib.Path("@paacerts@")
+diff --git a/matter_server/server/helpers/paa_certificates.py b/matter_server/server/helpers/paa_certificates.py
+index 9ac5a10..25230c1 100644
+--- a/matter_server/server/helpers/paa_certificates.py
++++ b/matter_server/server/helpers/paa_certificates.py
+@@ -58,84 +58,14 @@ async def fetch_dcl_certificates(
+     fetch_production_certificates: bool = True,
+ ) -> int:
+     """Fetch DCL PAA Certificates."""
+-    LOGGER.info("Fetching the latest PAA root certificates from DCL.")
+-    if not PAA_ROOT_CERTS_DIR.is_dir():
+-        loop = asyncio.get_running_loop()
+-        await loop.run_in_executor(None, makedirs, PAA_ROOT_CERTS_DIR)
+-    fetch_count: int = 0
+-    base_urls = set()
+-    # determine which url's need to be queried.
+-    # if we're going to fetch both prod and test, do test first
+-    # so any duplicates will be overwritten/preferred by the production version
+-    # NOTE: While Matter is in BETA we fetch the test certificates by default
+-    if fetch_test_certificates:
+-        base_urls.add(TEST_URL)
+-    if fetch_production_certificates:
+-        base_urls.add(PRODUCTION_URL)
+ 
+-    try:
+-        async with ClientSession(raise_for_status=True) as http_session:
+-            for url_base in base_urls:
+-                # fetch the paa certificates list
+-                async with http_session.get(
+-                    f"{url_base}/dcl/pki/root-certificates"
+-                ) as response:
+-                    result = await response.json()
+-                paa_list = result["approvedRootCertificates"]["certs"]
+-                # grab each certificate
+-                for paa in paa_list:
+-                    # do not fetch a certificate if we already fetched it
+-                    if paa["subjectKeyId"] in LAST_CERT_IDS:
+-                        continue
+-                    async with http_session.get(
+-                        f"{url_base}/dcl/pki/certificates/{paa['subject']}/{paa['subjectKeyId']}"
+-                    ) as response:
+-                        result = await response.json()
+-
+-                    certificate_data: dict = result["approvedCertificates"]["certs"][0]
+-                    certificate: str = certificate_data["pemCert"]
+-                    subject = certificate_data["subjectAsText"]
+-                    certificate = certificate.rstrip("\n")
+-
+-                    await write_paa_root_cert(
+-                        certificate,
+-                        subject,
+-                    )
+-                    LAST_CERT_IDS.add(paa["subjectKeyId"])
+-                    fetch_count += 1
+-    except ClientError as err:
+-        LOGGER.warning(
+-            "Fetching latest certificates failed: error %s", err, exc_info=err
+-        )
+-    else:
+-        LOGGER.info("Fetched %s PAA root certificates from DCL.", fetch_count)
+-
+-    return fetch_count
++    return 0
+ 
+ 
+ async def fetch_git_certificates() -> int:
+     """Fetch Git PAA Certificates."""
+-    fetch_count = 0
+-    LOGGER.info("Fetching the latest PAA root certificates from Git.")
+-    try:
+-        async with ClientSession(raise_for_status=True) as http_session:
+-            for cert in GIT_CERTS:
+-                if cert in LAST_CERT_IDS:
+-                    continue
+ 
+-                async with http_session.get(f"{GIT_URL}/{cert}.pem") as response:
+-                    certificate = await response.text()
+-                await write_paa_root_cert(certificate, cert)
+-                LAST_CERT_IDS.add(cert)
+-                fetch_count += 1
+-    except ClientError as err:
+-        LOGGER.warning(
+-            "Fetching latest certificates failed: error %s", err, exc_info=err
+-        )
+-
+-    LOGGER.info("Fetched %s PAA root certificates from Git.", fetch_count)
+-
+-    return fetch_count
++    return 0
+ 
+ 
+ async def fetch_certificates(
+@@ -144,12 +74,4 @@ async def fetch_certificates(
+ ) -> int:
+     """Fetch PAA Certificates."""
+ 
+-    fetch_count = await fetch_dcl_certificates(
+-        fetch_test_certificates=fetch_test_certificates,
+-        fetch_production_certificates=fetch_production_certificates,
+-    )
+-
+-    if fetch_test_certificates:
+-        fetch_count += await fetch_git_certificates()
+-
+-    return fetch_count
++    return 0
+