Skip to content

frost-net: domain-separated tagged digests for sign requests (deeper #524 fix) #529

Description

@kwsantiago

Follow-up to #524 (MVP policy seam PR landing now).

What the MVP shipped

PR #530 plumbed message_type into SessionInfo and added RefuseRawSignatureHooks, an opt-in pre-sign hook that rejects requests labeled message_type="raw". keep frost network serve --refuse-raw-sign installs it. This gives operators of hybrid Nostr + Bitcoin groups a way to stop blind-signing arbitrary 32-byte digests.

What's still missing

The MVP gate trusts the requester to label honestly. A malicious authorized requester can label a Bitcoin sighash as message_type="nostr-event" and the co-signer still blind-signs it.

Decision: Option 2 (responder recomputes from structured payload)

Considered options:

  1. Tagged digest: co-signers compute tagged_hash(message_type, request.message) and sign that. REJECTED. The resulting aggregate signature is valid under the group pubkey for the TAGGED hash, not the canonical digest, so it fails verification at Nostr relays (which verify against the canonical event id) and at Bitcoin full nodes (which verify against the BIP-341 sighash). Option 1 trades a real threat for an unusable signature.
  2. Responder recomputes from structured payload: CHOSEN. Preserves verifier-compatible signatures while cryptographically binding the label to the digest.

Design (Option 2)

For known structured message_type values, the requester sends the structured payload alongside the 32-byte digest. The responder recomputes the digest from the structured payload and refuses if it doesn't match request.message. The signature itself is still over request.message, so relays and full nodes verify it normally.

Per-domain plumbing:

  • message_type="nostr-event": payload is the serialized UnsignedEvent (pubkey, created_at, kind, tags, content). Responder runs UnsignedEvent::ensure_id() and confirms the result equals request.message. A spoofer claiming "nostr-event" for a Bitcoin sighash fails this check because the Nostr-canonical hash of the supplied event JSON won't equal the sighash bytes.
  • message_type="bitcoin-sighash": payload is the PSBT + input index + sighash type. Responder reconstructs the BIP-341 sighash and confirms. A spoofer claiming "bitcoin-sighash" for a Nostr event id fails because the reconstructed sighash differs from the event id bytes.
  • message_type="raw": no structured form to recompute. The MVP RefuseRawSignatureHooks policy owns this case; operators of hybrid groups install it via --refuse-raw-sign. Raw remains opt-out, not domain-validated.

Future structured domains slot in as new arms in the responder's recompute switch. The protocol stays open without adding new wire fields per domain (the structured payload is itself typed).

Wire format

Extend SignRequestPayload with an optional structured_payload: Option<Vec<u8>> whose interpretation is keyed by message_type. Backwards-compat: a peer running pre-#529 code sends None; the responder running #529 code falls back to current behavior (or refuses, depending on a group-level policy flag — design TBD per migration story below).

Migration story

Domain-validated signing is gated behind a group-level policy flag so existing groups don't break the moment one peer upgrades:

  • Default OFF on existing groups: responders accept unstructured requests as before.
  • Operators flip ON per-group once all participants have upgraded.
  • A future major release flips the default to ON.

In-flight sessions started before policy change use the policy in force when the request was received (no mid-session policy switches).

Acceptance

  • SignRequestPayload carries an optional structured payload typed by message_type.
  • Responder recomputes the digest for nostr-event and bitcoin-sighash and rejects on mismatch.
  • Test: a Bitcoin sighash labeled as nostr-event is refused.
  • Test: a Nostr event id labeled as bitcoin-sighash is refused.
  • Test: a correctly-labeled request from each domain produces a signature that verifies under the canonical verifier.
  • Group-level "require structured payload" policy flag, default OFF for backward compat.
  • MVP RefuseRawSignatureHooks continues to gate raw (deliberate — raw cannot be domain-validated).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthardnostr-frostNostr FROST coordination protocolp2PrioritysecuritySecurity-related issues

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions