Skip to content

Instantly share code, notes, and snippets.

@orangesurf
Last active February 12, 2026 21:44
Show Gist options
  • Select an option

  • Save orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c to your computer and use it in GitHub Desktop.

Select an option

Save orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c to your computer and use it in GitHub Desktop.

BIP322 Message Verification for Signing Devices

A proposal for displaying human-readable messages on hardware signing devices during BIP322 signature operations.

Problem

BIP322 defines a generic message signing format using virtual Bitcoin transactions. The message is reduced to a 32-byte hash (message_hash) embedded in the to_spend transaction's scriptSig:

vin[0].scriptSig = OP_0 PUSH32[ message_hash ]

When a signing device receives the to_sign PSBT, the to_spend transaction is embedded within it as the PSBT_IN_NON_WITNESS_UTXO. The device must traverse this structure to reach the hash, but only sees this hash

Goal

Enable signing devices to display the full human-readable message for user verification before signing, rather than just an opaque hash.

image

Proposed Solution

Transmit the original message to the signing device alongside the to_sign PSBT. The device verifies the message by computing its hash and comparing against the message_hash in the PSBT. If they match, the device displays the message for user confirmation.

image

Two complementary transfer methods are proposed:

Method 1: Sidecar File (SD Card)

Use the message hash as the filename for a file containing the original message.

{message_hash}.txt

Where message_hash is the lowercase hex encoding of sha256_tag(message) with tag BIP0322-signed-message.

Example: For message "Hello World", the hash is a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e, so the file would be:

a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e.txt

Why hash-as-filename:

  • Simple lookup: Device extracts to_spend from the to_sign PSBT's PSBT_IN_NON_WITNESS_UTXO, reads message_hash from its scriptSig, and checks for matching filename
  • No ambiguity: Creates 1:1 mapping between to_spend PSBT and message file
  • Multiple messages: Supports multiple pending signatures on same SD card

Method 2: QR Code (Camera)

The host wallet displays the message as a QR code (or animated QR for longer messages). The signing device scans it, hashes the content, and verifies against message_hash in the PSBT.

This method is useful when:

  • The device lacks an SD card slot
  • The user transferred the to_spend via QR and wants a fully air-gapped QR workflow

Signing Device Implementation

Flow

  1. Load to_sign transaction (SD card or QR)
  2. Extract to_spend from PSBT_IN_NON_WITNESS_UTXO of the first input, then extract message_hash from to_spend.vin[0].scriptSig
  3. Attempt to obtain the original message:
    • SD card: Check for {message_hash}.txt
    • QR scan: Prompt user to scan message QR from host
  4. Verify: compute sha256_tag(message) and compare to message_hash
    • Match: Display message for user verification
    • Mismatch: Show error, abort (message corrupted or tampered)
  5. User confirms or rejects
  6. If confirmed: Sign and export to_sign

Fallback: Hash-Only Mode

For advanced users who have verified the message externally, the device may offer an option to display only the raw message_hash and proceed without the full message. This should require explicit user acknowledgment.

Pseudocode

def verify_bip322_message(to_spend_psbt, message_source):
    # Extract to_spend from PSBT_IN_NON_WITNESS_UTXO
    to_spend = to_sign_psbt.inputs[0].non_witness_utxo

    # Extract message_hash from to_spend's scriptSig
    scriptsig = to_spend.vin[0].scriptSig
    message_hash = scriptsig[2:34]  # Skip OP_0 and push opcode
    
    # Obtain message (from file or QR scan)
    message = message_source.read()
    
    # Verify hash
    computed_hash = bip340_tagged_hash("BIP0322-signed-message", message)
    
    if computed_hash != message_hash:
        return Error("Message hash mismatch - possible tampering")
    
    return display_message_for_confirmation(message)

Host/Wallet Implementation

When preparing a BIP322 signature request:

  1. Compute message_hash = sha256_tag("BIP0322-signed-message", message)
  2. Generate to_spend transaction per BIP322
  3. Provide message to signing device via:
    • SD card: Save to_sign.psbt and {message_hash}.txt
    • QR workflow: Display to_sign QR, plus a "Show Message QR" option

Example

import hashlib

def sha256_tagged(tag: str, msg: bytes) -> bytes:
    tag_hash = hashlib.sha256(tag.encode()).digest()
    return hashlib.sha256(tag_hash + tag_hash + msg).digest()

message = b"This is my message"
message_hash = sha256_tagged("BIP0322-signed-message", message)

# Save files for SD card transfer
save_psbt("to_sign.psbt", generate_to_sign(message_hash, address))
save_file(f"{message_hash.hex()}.txt", message)

Security Considerations

Hash verification is mandatory. The device must always verify that sha256_tag(message) == message_hash. Cryptographic verification ensures the message actually corresponds to what will be signed. Never display or trust message contents without verification, a malicious actor could name any file with the expected hash.

Clear error states. If verification fails, the device must clearly indicate the mismatch and prevent signing. Ambiguous failures create attack vectors.

Design Rationale

Approach Pros Cons
PSBT proprietary field Self-contained Not standardized; requires ecosystem coordination; large PSBTs may be problematic for constrained devices
Fixed filename (e.g. message.txt) Simple Doesn't support multiple pending signatures
QR-only No file management Poor UX for long messages; requires camera
This proposal (file + QR) Flexible, supports multiple workflows Two methods to implement

Compatibility

This proposal requires no changes to BIP322 or BIP174. It is purely a convention for transporting the message preimage to signing devices. Devices that do not implement this proposal can still sign BIP322 messages—they simply cannot display the human-readable message to the user.

Reference

Authors

orangesurf / Mempool Research

License

This document is licensed under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/).

@natsoni
Copy link

natsoni commented Feb 2, 2026

When a signing device receives the to_spend PSBT, it only sees this hash—not the original message. Without the original message, the device cannot display what the user is actually signing, creating a significant UX and security gap.

Maybe it's already implied here and later, but the signing device receives the to_sign PSBT, and the to_spend transaction is embedded in the PSBT_IN_NON_WITNESS_UTXO

@orangesurf
Copy link
Author

Thanks, fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment