A proposal for displaying human-readable messages on hardware signing devices during BIP322 signature operations.
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
Enable signing devices to display the full human-readable message for user verification before signing, rather than just an opaque hash.
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.
Two complementary transfer methods are proposed:
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_spendfrom theto_signPSBT'sPSBT_IN_NON_WITNESS_UTXO, readsmessage_hashfrom its scriptSig, and checks for matching filename - No ambiguity: Creates 1:1 mapping between
to_spendPSBT and message file - Multiple messages: Supports multiple pending signatures on same SD card
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_spendvia QR and wants a fully air-gapped QR workflow
- Load
to_signtransaction (SD card or QR) - Extract
to_spendfromPSBT_IN_NON_WITNESS_UTXOof the first input, then extractmessage_hashfromto_spend.vin[0].scriptSig - Attempt to obtain the original message:
- SD card: Check for
{message_hash}.txt - QR scan: Prompt user to scan message QR from host
- SD card: Check for
- Verify: compute
sha256_tag(message)and compare tomessage_hash- Match: Display message for user verification
- Mismatch: Show error, abort (message corrupted or tampered)
- User confirms or rejects
- If confirmed: Sign and export
to_sign
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.
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)When preparing a BIP322 signature request:
- Compute
message_hash = sha256_tag("BIP0322-signed-message", message) - Generate
to_spendtransaction per BIP322 - Provide message to signing device via:
- SD card: Save
to_sign.psbtand{message_hash}.txt - QR workflow: Display
to_signQR, plus a "Show Message QR" option
- SD card: Save
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)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.
| 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 |
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.
- BIP322: Generic Signed Message Format
- BIP174: Partially Signed Bitcoin Transaction Format
- BIP340: Schnorr Signatures (tagged hashes)
orangesurf / Mempool Research
This document is licensed under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/).
Maybe it's already implied here and later, but the signing device receives the
to_signPSBT, and theto_spendtransaction is embedded in thePSBT_IN_NON_WITNESS_UTXO