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.
This commit is contained in:
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user