From e2a3d6e266258fe0cc207d84fec12af3d118b66f Mon Sep 17 00:00:00 2001 From: Pat Nakajima Date: Sun, 17 May 2026 22:01:34 -0700 Subject: [PATCH] add scripts --- .gitignore | 3 - Cargo.toml | 4 +- README.md | 21 ++++++- src/main.rs => examples/basic.rs | 0 releasor2000.toml | 36 ------------ scripts/provision-device.sh | 87 ++++++++++++++++++++++++++++ src/bin/toes-matter-creds.rs | 17 ++++++ src/lib.rs | 97 +++++++++++++++++++++++++++++--- 8 files changed, 214 insertions(+), 51 deletions(-) delete mode 100644 .gitignore rename src/main.rs => examples/basic.rs (100%) delete mode 100644 releasor2000.toml create mode 100755 scripts/provision-device.sh create mode 100644 src/bin/toes-matter-creds.rs diff --git a/.gitignore b/.gitignore deleted file mode 100644 index cf6578b..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/.vscode -/target -/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 2693ba1..3ec84e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toes-matter" -version = "0.1.1" +version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" readme = "README.md" @@ -19,7 +19,7 @@ env_logger = "0.11" futures-lite = "2" log = "0.4" rand = { version = "0.8", features = ["std", "std_rng"] } -rs-matter = { git = "https://git.fishmt.net/nakajima/rs-matter", branch = "bluez-ios-reconnect", default-features = false, features = [ +rs-matter = { path = "../../rs-matter/rs-matter", default-features = false, features = [ "log", "os", "rustcrypto", diff --git a/README.md b/README.md index e78c387..5ff82ac 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,23 @@ fn main() -> toes_matter::Result<()> { ## Development credentials -`generate_credentials()` writes rs-matter's built-in development/test DAC/PAI/CD plus setup QR/manual-code metadata. These are for local development, not production. +`generate_credentials()` writes rs-matter's built-in development/test DAC/PAI/CD plus setup QR/manual-code metadata. It creates a random setup passcode/discriminator the first time and reuses an existing `setup.txt` on later runs. These credentials are for local development, not production. + +## Post-flash headless provisioning + +After flashing a Linux device reachable over SSH: + +```bash +toes-matter/scripts/provision-device.sh user@device-host device-001 +``` + +The script generates `manufacturing/device-001/creds`, prints the manual code / QR payload, renders a terminal QR if `qrencode` is installed, copies the creds to `/var/lib/toes-matter/creds`, and restarts `toes-matter.service` if present. + +Set these if your service uses different paths: + +```bash +REMOTE_CREDS_DIR=/var/lib/my-app/creds \ +REMOTE_STATE_DIR=/var/lib/my-app/state \ +SERVICE=my-app.service \ +toes-matter/scripts/provision-device.sh user@device-host device-001 +``` diff --git a/src/main.rs b/examples/basic.rs similarity index 100% rename from src/main.rs rename to examples/basic.rs diff --git a/releasor2000.toml b/releasor2000.toml deleted file mode 100644 index 539a125..0000000 --- a/releasor2000.toml +++ /dev/null @@ -1,36 +0,0 @@ -[project] -name = "toes-matter" -auto-tag = true # detect Cargo.toml version, create/push git tag v before releasing to git -# binary = "toes-matter" # defaults to project name -# package = "toes-matter" # optional workspace package override; auto-detected from binary when unique -# binaries = ["toes-matter", "toes-matter-cli"] # optional extra release assets -repo = "nakajima/toes-matter" -# version-command = "git describe --tags --abbrev=0" - -[build] -command = "cargo build --release --target {target}" -artifact = "target/{target}/release/{binary}" -targets = [ - "aarch64-unknown-linux-gnu", -] - -[git] -type = "gitea" # defaults to "github" -base-url = "https://git.fishmt.net" -# api-base-url = "https://git.example.com/api/v1" # defaults from type/base-url -# token-env = "GITEA_TOKEN" # defaults: GITHUB_TOKEN or GITEA_TOKEN - -[channels.git] -enabled = true - -# [channels.homebrew] -# tap = "owner/homebrew-tap" -# formula-name = "toes-matter" - -# [channels.cargo] -# crate-name = "toes-matter" - -[channels.curl] - -# [channels.nix] -# flake-repo = "owner/nix-repo" # defaults to project repo diff --git a/scripts/provision-device.sh b/scripts/provision-device.sh new file mode 100755 index 0000000..cf98a87 --- /dev/null +++ b/scripts/provision-device.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Post-flash headless provisioning helper. +# +# Generates the Matter setup payload on this host, prints the QR/manual code, +# and copies the generated creds/config directory to a freshly flashed Linux device. +# +# Usage: +# toes-matter/scripts/provision-device.sh user@device-host [device-id] +# +# Environment: +# CREDS_DIR Local output dir. Default: toes-matter/manufacturing//creds +# REMOTE_CREDS_DIR Remote creds dir. Default: /var/lib/toes-matter/creds +# REMOTE_STATE_DIR Remote Matter state dir. Default: /var/lib/toes-matter/state +# SERVICE Optional service to restart. Default: toes-matter.service +# Set SERVICE= to skip restart. +# SSH_OPTS Extra ssh/scp options, e.g. '-p 2222' + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 user@device-host [device-id]" >&2 + exit 2 +fi + +TARGET="$1" +DEVICE_ID="${2:-$(date +%Y%m%d-%H%M%S)}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +CREDS_DIR="${CREDS_DIR:-$CRATE_DIR/manufacturing/$DEVICE_ID/creds}" +REMOTE_CREDS_DIR="${REMOTE_CREDS_DIR:-/var/lib/toes-matter/creds}" +REMOTE_STATE_DIR="${REMOTE_STATE_DIR:-/var/lib/toes-matter/state}" +SERVICE="${SERVICE-toes-matter.service}" +SSH_OPTS="${SSH_OPTS:-}" +REMOTE_TMP="/tmp/toes-matter-creds-$DEVICE_ID-$$" + +# shellcheck disable=SC2206 +SSH_ARGS=($SSH_OPTS) + +mkdir -p "$CREDS_DIR" + +echo "==> Generating development Matter credentials in $CREDS_DIR" +cargo run --quiet --manifest-path "$CRATE_DIR/Cargo.toml" --bin toes-matter-creds -- "$CREDS_DIR" + +SETUP_FILE="$CREDS_DIR/setup.txt" +MANUAL_CODE="$(awk -F= '$1 == "manual_code" { print $2 }' "$SETUP_FILE")" +QR_CODE="$(awk -F= '$1 == "qr_code" { print $2 }' "$SETUP_FILE")" + +echo +echo "==> Pairing info for device $DEVICE_ID" +echo "Manual code: $MANUAL_CODE" +echo "QR payload : $QR_CODE" + +if command -v qrencode >/dev/null 2>&1; then + echo + echo "==> QR code" + qrencode -t ANSIUTF8 "$QR_CODE" +else + echo + echo "Tip: install qrencode to render the QR in this terminal: sudo apt install qrencode" +fi + +echo +echo "==> Copying creds to $TARGET:$REMOTE_CREDS_DIR" +ssh "${SSH_ARGS[@]}" "$TARGET" "rm -rf '$REMOTE_TMP' && mkdir -p '$REMOTE_TMP'" +scp "${SSH_ARGS[@]}" -r "$CREDS_DIR"/. "$TARGET:$REMOTE_TMP/" +ssh "${SSH_ARGS[@]}" "$TARGET" "set -e +sudo rm -rf '$REMOTE_CREDS_DIR' +sudo mkdir -p '$(dirname "$REMOTE_CREDS_DIR")' '$REMOTE_STATE_DIR' +sudo mv '$REMOTE_TMP' '$REMOTE_CREDS_DIR' +sudo chmod -R go-rwx '$REMOTE_CREDS_DIR' '$REMOTE_STATE_DIR' +if [ -n '$SERVICE' ]; then + if systemctl list-unit-files '$SERVICE' >/dev/null 2>&1 || systemctl status '$SERVICE' >/dev/null 2>&1; then + sudo systemctl restart '$SERVICE' + else + echo 'Service $SERVICE not found; skipping restart' + fi +fi +" + +echo +echo "==> Done" +echo "Local copy saved at: $CREDS_DIR" +echo "Remote app env should use:" +echo " TOES_MATTER_CREDS_DIR=$REMOTE_CREDS_DIR" +echo " TOES_MATTER_STATE_DIR=$REMOTE_STATE_DIR" diff --git a/src/bin/toes-matter-creds.rs b/src/bin/toes-matter-creds.rs new file mode 100644 index 0000000..18e914c --- /dev/null +++ b/src/bin/toes-matter-creds.rs @@ -0,0 +1,17 @@ +#![recursion_limit = "256"] + +fn main() -> toes_matter::Result<()> { + env_logger::init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), + ); + + let mut args = std::env::args_os().skip(1); + let dir = args.next().unwrap_or_else(|| "./creds".into()); + + if args.next().is_some() { + eprintln!("Usage: toes-matter-creds [CREDS_DIR]"); + std::process::exit(2); + } + + futures_lite::future::block_on(toes_matter::generate_credentials(dir)) +} diff --git a/src/lib.rs b/src/lib.rs index 4ba5b46..4a04700 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,9 @@ use rs_matter::pairing::qr::{no_optional_data, CommFlowType, QrPayload, QrTextTy use rs_matter::pairing::DiscoveryCapabilities; use rs_matter::persist::{DirKvBlobStore, SharedKvBlobStore}; use rs_matter::respond::DefaultResponder; -use rs_matter::sc::pase::MAX_COMM_WINDOW_TIMEOUT_SECS; +use rs_matter::sc::pase::{ + Spake2pVerifierPassword, Spake2pVerifierPasswordRef, MAX_COMM_WINDOW_TIMEOUT_SECS, +}; use rs_matter::tlv::Nullable; use rs_matter::transport::network::btp::bluez; use rs_matter::transport::network::btp::{AdvData, Btp}; @@ -56,7 +58,7 @@ use rs_matter::utils::storage::pooled::PooledBuffers; use rs_matter::utils::sync::blocking::Mutex; use rs_matter::utils::sync::DynBase; use rs_matter::utils::zbus::Connection; -use rs_matter::{clusters, devices, root_endpoint, with, Matter, MATTER_PORT}; +use rs_matter::{clusters, devices, root_endpoint, with, BasicCommData, Matter, MATTER_PORT}; pub type Result = core::result::Result; @@ -141,10 +143,12 @@ pub async fn generate_credentials(path: impl AsRef) -> Result<()> { path.join("dac-private-key.raw"), TEST_DEV_ATT.dac_priv_key().access(), )?; + + let comm = read_comm_data(path)?.unwrap_or_else(random_comm_data); let payload = QrPayload::new_from_basic_info( DiscoveryCapabilities::BLE, CommFlowType::Standard, - TEST_DEV_COMM, + comm.clone(), &TEST_DEV_DET, no_optional_data, ); @@ -153,11 +157,13 @@ pub async fn generate_credentials(path: impl AsRef) -> Result<()> { let (qr_text, _) = payload.as_str(&mut qr_buf)?; let setup = format!( - "setup_passcode=20202021\n\ - discriminator=3840\n\ + "setup_passcode={}\n\ + discriminator={}\n\ manual_code={}\n\ qr_code={}\n", - TEST_DEV_COMM.compute_pretty_pairing_code(), + comm_passcode(&comm), + comm.discriminator, + comm.compute_pretty_pairing_code(), qr_text, ); @@ -166,6 +172,77 @@ pub async fn generate_credentials(path: impl AsRef) -> Result<()> { Ok(()) } +fn load_comm_data(config: &Config) -> Result { + match read_comm_data(&config.creds_dir)? { + Some(comm) => Ok(comm), + None => Ok(TEST_DEV_COMM.clone()), + } +} + +fn read_comm_data(path: &Path) -> Result> { + let setup_path = path.join("setup.txt"); + let setup = match std::fs::read_to_string(setup_path) { + Ok(setup) => setup, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err.into()), + }; + + let passcode = parse_setup_u32(&setup, "setup_passcode")?; + let discriminator = parse_setup_u32(&setup, "discriminator")?; + + if discriminator > 0x0fff || !valid_setup_passcode(passcode) { + return Err(ErrorCode::InvalidData.into()); + } + + Ok(Some(make_comm_data(passcode, discriminator as u16))) +} + +fn parse_setup_u32(setup: &str, key: &str) -> Result { + let value = setup + .lines() + .find_map(|line| line.strip_prefix(key)?.strip_prefix('=')) + .ok_or(ErrorCode::InvalidData)?; + + value.parse().map_err(|_| ErrorCode::InvalidData.into()) +} + +fn random_comm_data() -> BasicCommData { + let mut rng = rand::thread_rng(); + + let passcode = loop { + let passcode = (rng.next_u32() % 99_999_998) + 1; + if valid_setup_passcode(passcode) { + break passcode; + } + }; + + let discriminator = (rng.next_u32() & 0x0fff) as u16; + + make_comm_data(passcode, discriminator) +} + +fn make_comm_data(passcode: u32, discriminator: u16) -> BasicCommData { + BasicCommData { + password: Spake2pVerifierPassword::new_from_ref(Spake2pVerifierPasswordRef::new( + &passcode.to_le_bytes(), + )), + discriminator, + } +} + +fn comm_passcode(comm: &BasicCommData) -> u32 { + u32::from_le_bytes(*comm.password.access()) +} + +fn valid_setup_passcode(passcode: u32) -> bool { + const INVALID: &[u32] = &[ + 0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888, + 99999999, 12345678, 87654321, + ]; + + (1..=99_999_998).contains(&passcode) && !INVALID.contains(&passcode) +} + /// Provision this device over BLE Matter commissioning and Wi-Fi Network Commissioning. /// /// Returns immediately if a fabric is already stored in the configured state dir. @@ -218,7 +295,8 @@ async fn run_provision(connection: &Connection, net_ctl: N, config: &Config) where N: NetCtl + WifiDiag + NetChangeNotif, { - let mut matter = Matter::new_default(&TEST_DEV_DET, TEST_DEV_COMM, &TEST_DEV_ATT, MATTER_PORT); + let comm = load_comm_data(config)?; + let mut matter = Matter::new_default(&TEST_DEV_DET, comm.clone(), &TEST_DEV_ATT, MATTER_PORT); let mut networks = WifiNetworks::<3>::new(); let mut events: Events = Events::new_default(); let mut kv_buf = [0_u8; KV_BUF_SIZE]; @@ -281,7 +359,7 @@ where let btp = Btp::new(); btp.set_relaxed_mtu_nego(true); - let adv_data = AdvData::new(&TEST_DEV_DET, TEST_DEV_COMM.discriminator); + let adv_data = AdvData::new(&TEST_DEV_DET, comm.discriminator); let mut bluetooth = pin!(bluez::run_peripheral( connection, None, "TOES", &adv_data, &btp )); @@ -311,7 +389,8 @@ async fn run_listen(net_ctl: N, config: &Config) -> Result<()> where N: NetCtl + WifiDiag + NetChangeNotif, { - let mut matter = Matter::new_default(&TEST_DEV_DET, TEST_DEV_COMM, &TEST_DEV_ATT, MATTER_PORT); + let comm = load_comm_data(config)?; + let mut matter = Matter::new_default(&TEST_DEV_DET, comm, &TEST_DEV_ATT, MATTER_PORT); let mut networks = WifiNetworks::<3>::new(); let mut events: Events = Events::new_default(); let mut kv_buf = [0_u8; KV_BUF_SIZE];