Files
box/bin/dm-sign.sh
operator-main d82bf58e78 DM: signed-DM pipeline (dm-sign.sh) + attribution unification + honest docs
- New bin/dm-sign.sh: produce signed DMs (ssh-keygen -Y sign, namespace
  dm) in the [from:X] [id:Y] wire format that dm.py verify-sig checks.
  dm.py referenced it in usage but it never existed.
- dm.py: rewrite stale docstring/argparse (claimed verification was
  removed / delivery unconfirmed — it does recipient-side read-back with
  3 retries); unify all attribution on [from:X]/[id:Y] (was three
  formats: [from X], [X], [from:X]); fix dm_thread double-attribution;
  --no-verify kept as a documented no-op; raw mode sends verbatim and
  reuses the embedded [id:Y] for the audit log; navigation/send/park
  now all target the recipient browser (fixes cross-operator side-chat
  sends); drop dead --from-sender flag.
- Register dm-signers/operator-main.pub.
- Round-trip verified: sign (operator-main) -> send --raw via opm
  loopback -> read-back SENT+VERIFIED -> verify-sig GOOD.
2026-10-04 02:31:37 +00:00

75 lines
2.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# dm-sign.sh — produce a signed DM for the fleet DM system.
#
# Signs a message with an SSH key (namespace "dm", Ed25519) and prints the
# signed wire format that `dm.py verify-sig` checks:
#
# [from:<identity>] [id:<id>]
#
# <message body>
#
# -----BEGIN SSH SIGNATURE-----
# ...
# -----END SSH SIGNATURE-----
#
# The signed payload is exactly "[from:X] [id:Y]\n\n<body>" (no trailing
# newline) — verify-sig reconstructs it by stripping everything from the
# signature block onward, so the two must match byte-for-byte.
#
# Usage:
# dm-sign.sh --from <identity> [--key <privkey>] [--id <id>] <message>
#
# Defaults: --key ~/.ssh/id_frontdoor, --id = 8 random hex chars.
# The private key is only ever read locally; it is never moved or copied.
# Pipe the output straight into `dm.py send --raw` (never truncate it).
#
# Example:
# dm.py send --agent opm --target main --raw \
# "$(dm-sign.sh --from operator-main 'hello from the operator')"
set -euo pipefail
FROM=""
KEY="$HOME/.ssh/id_frontdoor"
ID="$(head -c4 /dev/urandom | od -An -tx1 | tr -d ' \n')"
usage() {
sed -n '2,/^set -euo/p' "$0" | sed 's/^# \?//'
}
while [[ $# -gt 0 ]]; do
case "$1" in
--from) FROM="${2:?--from needs a value}"; shift 2 ;;
--key) KEY="${2:?--key needs a value}"; shift 2 ;;
--id) ID="${2:?--id needs a value}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) echo "error: unknown option: $1" >&2; exit 1 ;;
*) break ;;
esac
done
if [[ $# -eq 0 ]]; then
echo "error: no message given" >&2
echo "usage: dm-sign.sh --from <identity> [--key <privkey>] [--id <id>] <message>" >&2
exit 1
fi
MESSAGE="$*"
[[ -n "$FROM" ]] || { echo "error: --from <identity> is required" >&2; exit 1; }
[[ -f "$KEY" ]] || { echo "error: private key not found: $KEY" >&2; exit 1; }
TD="$(mktemp -d)"
trap 'rm -rf "$TD"' EXIT
PAYLOAD="$TD/payload"
# Payload: header, blank line, body — NO trailing newline (verify-sig strips).
printf '[from:%s] [id:%s]\n\n%s' "$FROM" "$ID" "$MESSAGE" > "$PAYLOAD"
# Never reuse a stale signature: a leftover .sig from an earlier run would
# silently sign the wrong payload (burned 20 minutes on the board, 2026-10-03).
rm -f "$PAYLOAD.sig"
ssh-keygen -Y sign -f "$KEY" -n dm "$PAYLOAD" >/dev/null
printf '[from:%s] [id:%s]\n\n%s\n\n' "$FROM" "$ID" "$MESSAGE"
cat "$PAYLOAD.sig"