Robotics
Microduck
ONNX

A missing step before a policy loads: checking it is the file you meant to run

#4
by nootxlm - opened

Hello β€” a small, finished thing for anyone publishing or running Microduck policies, and one measurement only an owner can make.

Policies from this hub are downloaded, copied onto the robot, and loaded as they arrive β€” nothing checks that the file on the robot is the file its publisher released. We built that missing step at the desk: sign the policy file once, at release, with a post-quantum signature (ML-DSA-65, the FIPS 204 standard β€” the scheme built to stay valid against the machines ordinary signatures fall to), and verify the signature at load time, once, before the policy ever runs, never inside the control loop. The signature itself is 3,309 bytes, riding beside the policy in a small sidecar file.

It works end to end at the desk, on a copy of the shipped walk-getup policy loaded through the same load point the harness uses: two independent implementations on two machines cross-check each other's signatures and agree, a one-byte change anywhere in the file or the signature is refused before the policy loads, and verification takes a median of 3.3 milliseconds on one desktop machine and 4.6 on the other. One honest limit, stated plainly: the check proves the file arrived exactly as its publisher released it. It says nothing about whether a policy is good β€” that judgement stays with the runner, and a signed bad policy is still a bad policy.

The one number we cannot earn at the desk is what that verification costs on the duck's own processor. Every figure for it so far is arithmetic. So the capture is printed in full below β€” one short script and its one-command runner, worth reading before you run them: together they build a policy-sized test payload in memory, sign and verify it with a throwaway key, and write one small return file with the timings and the route they found. The run itself takes under a minute. Its expectations are printed with it in advance β€” it expects verification between 0.2 and 20 milliseconds on the duck's processor β€” so the return grades itself, and a return outside that band is the more interesting result. And "no post-quantum route exists on this unit yet" is a genuinely useful answer, not a failed run. Nothing is installed, nothing on the robot changes, and the return carries no identifying information.

Save the first block as pq-bench.mjs and the second as run.sh, side by side in one folder, and run sh run.sh on the duck's own board β€” the capture measures the machine it runs on, so it has to run on the duck itself.

// PKG-PQ-BENCH capture instrument β€” ML-DSA-65 route census + timing on THIS unit.
// Self-contained: builds its policy-sized payload in memory, signs/verifies with a
// throwaway key, prints one JSON return. No files written, no network, no installs.
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';

const POLICY_BYTES = 793814; // the shipped walk-getup policy's exact size
const out = {
  package: 'PKG-PQ-BENCH/1',
  route: 'NONE',
  platform: os.platform(), arch: os.arch(),
  node: process.version, openssl: process.versions.openssl || null,
  cpu: null, verify_median_ms: null, sign_median_ms: null,
  signature_bytes: null, verifying_key_bytes: null, iterations: 0,
};
try {
  const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
  const m = cpuinfo.match(/model name\s*:\s*(.+)/) || cpuinfo.match(/Model\s*:\s*(.+)/) || cpuinfo.match(/Hardware\s*:\s*(.+)/);
  if (m) out.cpu = m[1].trim();
} catch { /* cpu stays null β€” reported, not estimated */ }

try {
  const { publicKey, privateKey } = crypto.generateKeyPairSync('ml-dsa-65');
  const spki = publicKey.export({ type: 'spki', format: 'der' });
  const payload = Buffer.alloc(POLICY_BYTES);
  for (let i = 0; i < POLICY_BYTES; i++) payload[i] = (i * 31 + 7) & 0xff;
  const sig = crypto.sign(null, payload, privateKey);
  if (!crypto.verify(null, payload, publicKey, sig)) throw new Error('self-verify failed');
  const v = [], s = [];
  for (let i = 0; i < 50; i++) {
    let t = performance.now(); crypto.verify(null, payload, publicKey, sig); v.push(performance.now() - t);
    if (i < 20) { t = performance.now(); crypto.sign(null, payload, privateKey); s.push(performance.now() - t); }
  }
  const med = a => a.slice().sort((x, y) => x - y)[Math.floor(a.length / 2)];
  out.route = 'NODE';
  out.verify_median_ms = +med(v).toFixed(3);
  out.sign_median_ms = +med(s).toFixed(3);
  out.signature_bytes = sig.length;
  out.verifying_key_bytes = spki.length - 22; // SPKI DER minus its fixed header
  out.iterations = 50;
} catch (e) {
  out.route = 'NONE';
  out.note = String(e.message || e).slice(0, 120);
}
console.log(JSON.stringify(out, null, 2));
#!/bin/sh
# PKG-PQ-BENCH β€” one-command capture. Writes exactly one return file: ./pq-return.json
# Read-only apart from that file. No installs, no network.
set -eu
cd "$(dirname "$0")"
if command -v node >/dev/null 2>&1; then
  node pq-bench.mjs > pq-return.json 2>/dev/null || node pq-bench.mjs > pq-return.json
else
  cat > pq-return.json <<'EOF'
{
  "package": "PKG-PQ-BENCH/1",
  "route": "NONE",
  "note": "node not present on this unit β€” census result, reported as-is"
}
EOF
fi
echo "Return file written: pq-return.json β€” send it back unedited."
cat pq-return.json

If you run it, the one file it writes, pq-return.json, comes back here as a reply β€” paste its contents in whole, unedited. Whatever it says becomes the measured number, and the arithmetic retires. And if your duck has not landed yet, the package keeps β€” the step is worth setting now, while the fleet is still arriving, and the number matters the moment the first units are running policies from this hub.

Thank you for building with these small machines.

Sign up or log in to comment