# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.
"""BIP322 signed messages: a script is satisfied, not a key recovered.
`ecc.bms` signs with a key and lets the verifier recover it, which is why
it can only speak about the addresses that *are* a public key hash --
p2pkh, and by Electrum's extension the two p2wpkh spellings. A taproot
address is a tweaked BIP340 key and a p2wsh address is a hash of a
script, and no recovery flag names either.
BIP322 asks the other question. The address becomes the script_pub_key of
a virtual output, and the signature is whatever spends it: a witness
stack, a whole transaction, or a psbt. Verification is then the script
interpreter -- `script.engine` -- rather than a key comparison, so every
script btclib can run is a script that can sign, multisig and timelocks
included. The signature commits to the public key too, which the compact
ECDSA of BMS does not.
The two virtual transactions are the whole of the construction:
- `to_spend` pays 0 satoshi to the address, and is spendable by nobody:
its single input is the null outpoint of a coinbase, and its
script_sig is `OP_0 PUSH32 message_hash`, the BIP340-tagged hash of
the message under the `BIP0322-signed-message` tag. Message and
address are therefore both inside its txid
- `to_sign` spends that output and pays 0 satoshi to an `OP_RETURN`.
Its witness -- or script_sig -- is the signature
A verifier rebuilds `to_spend` from the message and the address it was
given, so a signature made for another message or another address spends
a different output and satisfies nothing. Neither transaction can be
broadcast, `to_spend`'s own input being unspendable.
Three encodings, all base64 with a three-character prefix in front of it,
and `Sig` holds whichever came:
- `smp`, the *simple* variant: the witness stack alone, which is enough
where the rest of `to_sign` is fixed -- native segwit, i.e. p2wpkh,
p2wsh and p2tr
- `ful`, the *full* variant: the whole `to_sign` transaction, which is
what a script_sig (p2pkh, p2sh), a version or a lock time needs
- `pof`, the *proof of funds* variant: a finalized psbt of `to_sign`,
carrying further inputs the signer also controls, with the utxo of
each. Whether those outputs exist and are unspent is the chain's
answer and not this module's
A signature with no prefix is read as *simple*, which BIP322 allows for
compatibility with the implementations that predate the prefixes. The
*legacy* variant is BMS: `assert_as_valid` hands a 65-byte compact
signature to `ecc.bms`, and only for a p2pkh address, the BIP restricting
it to that one.
Verification answers three states, as the BIP does. Valid is a return;
invalid is a `BTClibValueError` or a `BTClibRuntimeError`, whatever
failed being what it says; and *inconclusive* is `InconclusiveError`,
which is the state for a signature that today's rules cannot judge -- a
`to_sign` whose version is neither 0 nor 2, an upgradeable NOP, a witness
program of a version this library does not know. Each of those classes
is a `BTClibException`, so a single `except` catches whichever comes.
`verify` collapses all three to a boolean, and an inconclusive signature
is not a valid one.
What is enforced is BIP322's list, through the engine's own flags: the
consensus rules, then LOW_S, STRICTENC, NULLFAIL, MINIMALDATA,
CLEANSTACK, MINIMALIF and CONST_SCRIPTCODE for the required ones, and the
``DISCOURAGE_`` family for the upgradeable ones -- the backticks because a
name ending in an underscore is a link reference to docutils, which
sphinx runs with `-W`. The one rule of the list that is not a flag is
"all signatures MUST use SIGHASH_ALL", which no set of flags can express:
it is a rule about the stack elements the interpreter consumed as
signatures, and which elements those were is not readable from the
witness -- the control block of a single-leaf taproot tree is 65 bytes,
exactly the shape of a BIP340 signature with an explicit hash type. So
the engine reports them, through `verify_input`'s `hash_types`, and the
rule is enforced over what it reports.
The fourth flow of the BIP is not a signature encoding at all: a
multisig signature is coordinated as a psbt, and the psbt says what is
being signed through `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09`, the
global field BIP322 adds to BIP174's registry. `Psbt.signed_message`
holds it; here `to_sign_psbt` is the Creator that writes one and
`signed_message` the question a Signer puts to what it received --
"is this a BIP322 psbt, and for which message" -- so that a device shows
"signing message m for address A" rather than "spending 0 satoshi",
which is the promise the field exists to let it keep.
https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
from btclib.alias import Octets, String
from btclib.curves import bytes_from_prv_key_int
from btclib.ecc import bms, dsa, ssa
from btclib.exceptions import (
BTClibRuntimeError,
BTClibValueError,
InconclusiveError,
)
from btclib.hashes import hash160, tagged_hash
from btclib.key import PrvKeyData, PubKeyData
from btclib.script import serialize
from btclib.script.engine import ALL_FLAGS, ScriptFlag, verify_transaction
from btclib.script.script_pub_key import ScriptPubKey, type_and_payload
from btclib.script.sig_hash import ALL, DEFAULT, from_tx
from btclib.script.sig_hash import taproot as taproot_sig_hash
from btclib.script.taproot import output_prvkey_from_merkle_root, output_pubkey
from btclib.script.witness import Witness
from btclib.tx import OutPoint, Tx, TxIn, TxOut
from btclib.utils import assert_type, bytes_from_octets, str_from_string
from btclib_wallet.psbt import Psbt, extract_tx
__all__ = [
"FULL",
"PROOF_OF_FUNDS",
"REQUIRED_RULES",
"SIMPLE",
"TAG",
"UPGRADEABLE_RULES",
"Sig",
"assert_as_valid",
"assert_signed_message",
"message_hash",
"sign",
"signed_message",
"to_sign",
"to_sign_psbt",
"to_spend",
"verify",
]
TAG = b"BIP0322-signed-message"
SIMPLE = "smp"
FULL = "ful"
PROOF_OF_FUNDS = "pof"
_PREFIXES = (SIMPLE, FULL, PROOF_OF_FUNDS)
# every prefix is three characters, so the split of a signature into
# prefix and base64 does not depend on which one it is
_PREFIX_SIZE = 3
# the output of `to_sign`, which is the same for every message and every
# address: an OP_RETURN paying nothing, provably unspendable
_OP_RETURN = b"\x6a"
# BMS is 65 octets, and no witness stack is: the shortest one is a single
# 64-byte BIP340 signature, which the count and the push length make 66.
# So the two encodings do not overlap and a prefixless signature can be
# read as whichever of the two it is
_BMS_SIZE = 65
# the rules of BIP322's "Check the required rules", plus the consensus
# ones every spend is held to. Failing one of these is *invalid*: the
# signature is malleable, or not canonical, or does not spend
REQUIRED_RULES = (
ALL_FLAGS
| ScriptFlag.STRICTENC
| ScriptFlag.LOW_S
| ScriptFlag.NULLFAIL
| ScriptFlag.MINIMALDATA
| ScriptFlag.CLEANSTACK
| ScriptFlag.MINIMALIF
| ScriptFlag.CONST_SCRIPTCODE
)
# BIP322's "Check the upgradeable rules". Failing one of these is
# *inconclusive* and not invalid, which is the distinction the two sets
# exist for: a NOP reserved for an upgrade and a witness program of an
# unknown version are anyone-can-spend today and may mean something
# tomorrow, so a validator that judged them valid would be judging a
# script it does not know.
#
# The BIP names the two pre-taproot ones. The three BIP342 added are the
# same rule for tapscript -- an OP_SUCCESS, an unknown public key type
# and an unknown leaf version are each successful *because* nothing
# checks them -- and are here for the reason the BIP gives for the other
# two, rather than because it lists them
UPGRADEABLE_RULES = (
ScriptFlag.DISCOURAGE_UPGRADABLE_NOPS
| ScriptFlag.DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM
| ScriptFlag.DISCOURAGE_UPGRADABLE_PUBKEYTYPE
| ScriptFlag.DISCOURAGE_OP_SUCCESS
| ScriptFlag.DISCOURAGE_UPGRADABLE_TAPROOT_VERSION
)
# BIP322's "The version of to_sign must be 0 or 2": 0 is what a signature
# with no time lock uses, and 2 is what BIP68 relative lock times need
_VERSIONS = (0, 2)
[docs]
def message_hash(msg: Octets) -> bytes:
"""Return the BIP340-tagged hash of the message, under BIP322's tag.
The message enters as it is: no magic string around it, no length in
front of it, and no terminator after it. The tag is what keeps this
hash from meaning anything under any other protocol, which is the
job BMS gives to its "Bitcoin Signed Message:" envelope.
"""
return tagged_hash(TAG, bytes_from_octets(msg))
[docs]
def to_spend(msg: Octets, script_pub_key: Octets) -> Tx:
"""Return the virtual transaction the message and the script commit to.
Nobody can spend it and nobody can broadcast it: its one input is
the null outpoint a coinbase carries, and a coinbase is valid in a
block and nowhere else. What it is for is its txid, which the
message hash in its script_sig and the challenge script in its
output both enter -- so a `to_sign` built on this txid is a
signature for this message and this address alone.
"""
script_sig = b"\x00\x20" + message_hash(msg)
tx_in = TxIn(OutPoint(), script_sig, 0, Witness())
tx_out = TxOut(0, bytes_from_octets(script_pub_key))
return Tx(0, 0, [tx_in], [tx_out])
[docs]
def to_sign(
to_spend_tx: Tx,
script_sig: Octets = b"",
witness: Witness | None = None,
*,
version: int = 0,
lock_time: int = 0,
sequence: int = 0,
extra_inputs: list[TxIn] | None = None,
) -> Tx:
"""Return the virtual transaction that spends `to_spend_tx`.
The signature is `script_sig`, `witness`, or both: what satisfies
the challenge script, whichever half of an input carries it.
The three keyword arguments are the fields the *full* variant may
set and the *simple* variant may not, all three of them 0 there: a
version of 2 and a lock time for a `CHECKLOCKTIMEVERIFY` script, a
sequence for a `CHECKSEQUENCEVERIFY` one. `extra_inputs` are the
outputs a proof of funds shows control of, appended after the one
input every signature has.
"""
tx_in = TxIn(OutPoint(to_spend_tx.id, 0), script_sig, sequence, witness)
vin = [tx_in, *extra_inputs] if extra_inputs else [tx_in]
return Tx(version, lock_time, vin, [TxOut(0, _OP_RETURN)])
[docs]
def to_sign_psbt(msg: Octets, addr: String) -> Psbt:
"""Return the psbt a Creator hands the signers of this challenge.
BIP322's fourth flow: a signature that several keys make together is
coordinated as a psbt, so what a Signer receives is the unsigned
`to_sign` rather than a witness stack to fill in. Two fields make it
one -- the message, in the global
`PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE`, and the output being spent,
without which no signature can be made -- and `signed_message` is
the question this answers on the other side.
The whole of `to_spend` goes in as the non-witness utxo rather than
its one output as a witness utxo: it is the answer for a challenge
script of any type, where a witness utxo is the answer for the
segwit ones alone, and the transaction is virtual but it is a
transaction. Nothing is signed here, and nothing is finalized: what
comes back is what a Signer signs, `psbt.sign` and `psbt.finalize`
being the roles that follow, and `Sig` of the finalized psbt the
proof-of-funds encoding.
"""
script_pub_key = ScriptPubKey.from_address(addr).script
spend = to_spend(msg, script_pub_key)
psbt = Psbt.from_tx(to_sign(spend))
psbt.inputs[0].non_witness_utxo = spend
psbt.signed_message = bytes_from_octets(msg)
return psbt
[docs]
@dataclass(frozen=True)
class Sig:
"""A BIP322 signature: what the variant carries, and nothing beside it.
One field, because the variant is not a second fact: a witness stack
*is* a simple signature, a transaction a full one, and a psbt a
proof of funds, so `variant` reads the payload rather than being
stored where it could disagree with it.
There is no `parse`, and that is the format rather than an omission:
the three payloads are three unrelated serializations and only the
prefix of the text form says which one follows, so `b64decode` is
where a signature is read and `b64encode` where it is written.
"""
payload: Witness | Tx | Psbt
@property
def variant(self) -> str:
"""Return the three-character prefix this signature is written with."""
if isinstance(self.payload, Witness):
return SIMPLE
return FULL if isinstance(self.payload, Tx) else PROOF_OF_FUNDS
[docs]
def serialize(self, *, check_validity: bool = True) -> bytes:
"""Return the payload's own serialization, without the prefix.
The transaction is serialized with its witness, that being where
a full signature keeps the signature.
"""
if isinstance(self.payload, Tx):
return self.payload.serialize(
include_witness=True, check_validity=check_validity
)
return self.payload.serialize(check_validity=check_validity)
[docs]
def b64encode(self, *, check_validity: bool = True) -> str:
"""Return the signature as BIP322 writes it: prefix, then base64."""
data = self.serialize(check_validity=check_validity)
return self.variant + base64.b64encode(data).decode("ascii")
[docs]
@classmethod
def b64decode(cls: type[Sig], data: String, *, check_validity: bool = True) -> Sig:
"""Return the signature the text encodes, whichever variant it is.
A prefix that is not one of the three is not stripped and not
guessed at: it is base64 or it is nothing, and the three
characters are then part of the witness stack, which is what
refuses it. Absent a prefix the signature is *simple*, which is
what BIP322 says a verifier may assume of the implementations
that predate them.
"""
# the coercion before the strip, as in `ecc.bms.Sig.b64decode`
# and for its reason: what is neither text nor bytes left
# `bip322.verify` as an AttributeError about a missing method
# rather than as a refusal of the argument (issue btclib-org/btclib#814)
text = str_from_string(data, "base64 signature").strip()
prefix = text[:_PREFIX_SIZE]
if prefix not in _PREFIXES:
prefix = SIMPLE
else:
text = text[_PREFIX_SIZE:]
try:
payload_bin = base64.b64decode(text.encode("ascii"), validate=True)
except ValueError as e: # binascii.Error and UnicodeEncodeError
raise BTClibValueError(f"invalid base64 encoding: {e}") from e
if prefix == SIMPLE:
return cls(Witness.parse(payload_bin, check_validity=check_validity))
if prefix == FULL:
return cls(Tx.parse(payload_bin, check_validity=check_validity))
return cls(Psbt.parse(payload_bin, check_validity=check_validity))
def _psbt_prevouts(psbt: Psbt) -> list[TxOut]:
"""Return the output each input of a proof of funds spends.
`psbt.prevouts` is the same answer without BIP322's one addition:
"the Non-Witness UTXO field may be omitted for any input that spends
an output from the same transaction as an input earlier in the
list", which is what keeps a proof over many outputs of one
transaction from carrying that transaction many times.
"""
transactions: dict[bytes, Tx] = {}
outs: list[TxOut] = []
for i, psbt_in in enumerate(psbt.inputs):
if psbt_in.non_witness_utxo is not None:
transactions[psbt_in.non_witness_utxo.id] = psbt_in.non_witness_utxo
vout = psbt_in.output_index or 0
prev_tx = transactions.get(psbt_in.previous_tx_id or b"")
if psbt_in.witness_utxo is not None:
outs.append(psbt_in.witness_utxo)
elif prev_tx is not None and vout < len(prev_tx.vout):
outs.append(prev_tx.vout[vout])
else:
raise BTClibValueError(f"no utxo for input {i}")
return outs
def _assert_op_return_output(tx: Tx) -> None:
"""Refuse a `to_sign` whose output is not the one BIP322 fixes.
Exactly one, paying nothing, to an OP_RETURN: the output every
`to_sign` has, whatever the message and whatever the challenge. Its
own function because a verifier and a Signer ask it of two different
things -- a transaction that carries a signature, and a psbt that is
about to -- and one rule read in two places is one rule.
"""
if len(tx.vout) != 1:
raise BTClibValueError(f"{len(tx.vout)} outputs in to_sign instead of one")
if tx.vout[0].value or tx.vout[0].script_pub_key.script != _OP_RETURN:
raise BTClibValueError("the output of to_sign is not the OP_RETURN of 0")
def _assert_shape(tx: Tx, spend: Tx, prevouts: list[TxOut]) -> None:
"""Refuse a `to_sign` that is not one, the signature apart.
BIP322's "confirm all fields are set as specified above", in its
order: at least one input, the first of them spending the output of
`to_spend` -- which is where the message and the address are -- a
utxo for each of the rest, and exactly one output, the OP_RETURN
that pays nothing. The version is checked by `_assert_upgradeable`,
which is the state it belongs to.
"""
if not tx.vin:
raise BTClibValueError("no input in to_sign")
if tx.vin[0].prev_out != OutPoint(spend.id, 0):
err_msg = "the first input does not spend to_spend: "
err_msg += f"{tx.vin[0].prev_out.tx_id.hex()} instead of {spend.id.hex()}"
raise BTClibValueError(err_msg)
if len(prevouts) != len(tx.vin):
err_msg = f"{len(prevouts)} utxos for {len(tx.vin)} inputs"
raise BTClibValueError(err_msg)
_assert_op_return_output(tx)
def _assert_upgradeable(tx: Tx) -> None:
"""Refuse, as inconclusive, a version no BIP322 signature may have."""
if tx.version not in _VERSIONS:
err_msg = f"to_sign version {tx.version}: BIP322 allows 0 and 2"
raise InconclusiveError(err_msg)
def _assert_hash_types(hash_types: list[int]) -> None:
"""Refuse a signature that does not commit to the whole of `to_sign`.
BIP322's sixth required rule: "all signatures MUST use the
SIGHASH_ALL flag, unless the output type supports SIGHASH_DEFAULT,
which then MAY be used alternatively" -- so the two are one set
here, taproot being where the second is a spelling of the first and
the encodings refusing it elsewhere anyway.
What the rule buys is not the same at every input. The first
commits to the outpoint it spends whatever its hash type, and that
outpoint is `to_spend`'s txid, which the message and the challenge
script both enter; so the binding to the message survives a lax
hash type there, and what it loses is a commitment to an output
that is the constant OP_RETURN of 0. A further input of a proof of
funds has no such floor: ANYONECANPAY does not commit to the other
inputs, so a signature lifted out of the transaction that really
spent that utxo would satisfy it here -- a proof of control over
coins the prover never controlled.
"""
for hash_type in hash_types:
if hash_type not in (ALL, DEFAULT):
err_msg = f"hash type {hash_type:#04x}: BIP322 requires SIGHASH_ALL"
raise BTClibValueError(err_msg)
def _assert_scripts(prevouts: list[TxOut], tx: Tx) -> None:
"""Run the engine, telling an invalid signature from an inconclusive one.
The strict set first, which is the answer in the ordinary case and
one pass. Only where that fails is the required set run on its own,
and it is the second run that says which state this is: a script
that satisfies the required rules and not the upgradeable ones is
inconclusive, and one that fails the required rules is invalid --
which is the exception the second run raises by itself.
Each run reports its own hash types, and only a run that reached the
end of every script reports them all -- so the second run has a list
of its own rather than adding to what the first got as far as. The
rule is checked on either path because a required rule broken is
*invalid* whatever the upgradeable ones say, which is the order
BIP322 puts the two sets in.
"""
hash_types: list[int] = []
try:
verify_transaction(
prevouts, tx, REQUIRED_RULES | UPGRADEABLE_RULES, hash_types=hash_types
)
except (ValueError, BTClibRuntimeError) as e:
required_only: list[int] = []
verify_transaction(prevouts, tx, REQUIRED_RULES, hash_types=required_only)
_assert_hash_types(required_only)
raise InconclusiveError(f"upgradeable rule: {e}") from e
_assert_hash_types(hash_types)
[docs]
def assert_as_valid(
msg: Octets, addr: String, sig: Sig | String, *, legacy: bool = True
) -> None:
"""Refuse a signature that does not spend the address's own output.
The message and the address rebuild `to_spend` here, so what the
signature is checked against is never what it claims to be: a
signature for another message, or for another address, satisfies a
script that is not this one.
`legacy` accepts a BMS signature -- the 65-byte compact one, with no
prefix -- for a p2pkh address, which is the compatibility BIP322
keeps and the only address type it keeps it for. False refuses it,
for a caller that wants BIP322 proper and nothing else.
Raises `InconclusiveError` for a signature that is not invalid and
cannot be judged valid; see the module docstring for that state.
"""
# which of the two verifications runs, a BMS signature over the message
# or BIP322's own, so it is a kind and `verify` inherits the check
assert_type(legacy, bool, "legacy")
script_pub_key = ScriptPubKey.from_address(addr).script
# `not a Sig` rather than a list of the String spellings: the
# question here is whether the signature is still text to be read,
# and a list of spellings answers it wrongly the moment `String`
# gains one -- a bytearray took the BIP322 branch in silence
if legacy and not isinstance(sig, Sig) and _is_bms(sig):
if type_and_payload(script_pub_key)[0] != "p2pkh":
err_msg = "a legacy signature is for a p2pkh address alone"
raise BTClibValueError(err_msg)
bms.assert_as_valid(msg, addr, sig)
return
sig = sig if isinstance(sig, Sig) else Sig.b64decode(sig)
spend = to_spend(msg, script_pub_key)
payload = sig.payload
# the psbt's own utxo for the first input is not consulted, whichever
# variant this is: what binds the signature to the message is the
# output computed just above, and a psbt disagreeing with it would be
# answering a different challenge under this message's name
prevouts = list(spend.vout)
if isinstance(payload, Witness):
tx = to_sign(spend, witness=payload)
elif isinstance(payload, Tx):
tx = payload
else:
tx = extract_tx(payload)
prevouts += _psbt_prevouts(payload)[1:]
_assert_shape(tx, spend, prevouts)
_assert_upgradeable(tx)
_assert_scripts(prevouts, tx)
def _assert_structurally_valid_(addr: String, sig: Sig | String) -> None:
"""Raise for an address or a signature that cannot possibly be one.
Ahead of the try that turns everything else -- a signature for
another message, a script the engine does not satisfy, a legacy
signature offered for an address that is not p2pkh -- into False
(issue btclib-org/btclib#2181).
The address is the challenge: it becomes the script_pub_key of
`to_spend`, so a string that decodes to no address leaves nothing to
build one from, and it is refused by the very parse
`assert_as_valid` dispatches on. A real address the signature does
not spend stays False, that being an answer about the signature.
What is readable as a signature here is BIP322's own encodings,
which `Sig.b64decode` reads, and the 65-octet compact signature the
legacy variant carries, which `_is_bms` recognises by its size; text
that is neither is refused. `legacy` does not enter that question:
it is a policy over schemes rather than a fact about the octets, so
a compact signature offered where `legacy=False` asked for BIP322
proper is False, beside the restriction to p2pkh that is the other
thing the keyword decides.
`check_validity=False` leaves the value half of the signature where
it belongs, as in `ecc.bms._assert_structurally_valid_`:
`assert_as_valid` reads the text again with its checks on, below,
inside the try.
The message is not asked here, as in `ecc.bms`: it is what the
signature is verified about, and issue btclib-org/btclib#814 is where that
was settled.
"""
ScriptPubKey.from_address(addr)
if not isinstance(sig, Sig) and not _is_bms(sig):
Sig.b64decode(sig, check_validity=False)
[docs]
def verify(
msg: Octets, addr: String, sig: Sig | String, *, legacy: bool = True
) -> bool:
"""Verify the BIP322 signature of a message for an address.
Raises where the address or the signature is structurally invalid --
no address at all, or text no encoding this module reads is written
in -- and answers False for a well-formed pair that is merely not
authentic. See `_assert_structurally_valid_`.
False for an inconclusive signature as well as for an invalid one:
the two states are worth telling apart, and `assert_as_valid` is
where they are, but neither of them is a signature that verified.
"""
_assert_structurally_valid_(addr, sig)
# ValueError and BTClibRuntimeError: a signature that does not
# satisfy the script is False, and so are the two refusals `legacy`
# decides -- a compact signature where BIP322 proper was asked for,
# and one offered for an address that is not p2pkh; a caller's own
# mistake in the address or in the signature's encoding is refused
# above rather than excluded from the except
try:
assert_as_valid(msg, addr, sig, legacy=legacy)
except (ValueError, BTClibRuntimeError):
return False
return True
def _challenge_script(psbt: Psbt) -> bytes:
"""Return the script_pub_key the psbt's first input spends.
Either utxo field answers it, and a psbt carries whichever its
Updater had. The non-witness one is indexed without a bounds check
because `assert_valid` is what the caller ran first: an outpoint
naming an output that transaction does not have is what it refuses.
"""
psbt_in = psbt.inputs[0]
if psbt_in.witness_utxo is not None:
return psbt_in.witness_utxo.script_pub_key.script
if psbt_in.non_witness_utxo is None:
raise BTClibValueError("no utxo for the first input")
return psbt_in.non_witness_utxo.vout[
psbt_in.output_index or 0
].script_pub_key.script
[docs]
def assert_signed_message(psbt: Psbt) -> bytes:
"""Return the message this psbt is the BIP322 challenge of, or refuse.
The Signer's own question, and it is not "does the psbt carry a
message": a message that the transaction does not commit to is what
a device showing "signing message m" would be lying about. So the
field is one of five conditions and the other four are the psbt
being a `to_sign` -- an input to spend, its outpoint being output 0
of the `to_spend` this very message and this very challenge script
rebuild, and the one output that pays nothing to an OP_RETURN.
The challenge script comes from the psbt itself, which is what
leaves the caller nothing to be told: `assert_as_valid` is handed an
address and checks a signature against it, where a Signer has not
been told an address and is working out what it would be signing.
"""
psbt.assert_valid()
msg = psbt.signed_message
if msg is None:
raise BTClibValueError("no signed message in the psbt")
if not psbt.inputs:
raise BTClibValueError("no input in to_sign")
tx = psbt.tx
spend = to_spend(msg, _challenge_script(psbt))
if tx.vin[0].prev_out != OutPoint(spend.id, 0):
err_msg = "the first input does not spend to_spend: "
err_msg += f"{tx.vin[0].prev_out.tx_id.hex()} instead of {spend.id.hex()}"
raise BTClibValueError(err_msg)
_assert_op_return_output(tx)
return msg
[docs]
def signed_message(psbt: Psbt) -> bytes | None:
"""Return the message the psbt signs, or None if it signs no message.
`assert_signed_message` collapsed to what a signing device does with
it: a message to show in place of the spend, or nothing and the
spend as usual. None is both "no such field" and "a field the
transaction does not bear out", the second being the one worth an
exception, so a caller that has to tell them apart asks the other
one and reads what it says.
"""
# as `verify` catches them, and for its reasons: a psbt that is not
# a BIP322 one is an answer rather than an error, and a TypeError is
# the caller handing this something that is not a psbt
try:
return assert_signed_message(psbt)
except (ValueError, BTClibRuntimeError):
return None
def _is_bms(sig: String) -> bool:
"""Whether the text is a BMS compact signature rather than a BIP322 one.
The size settles it, base64 being the encoding of both: 65 octets is
a recovery flag and an ECDSA signature, and no witness stack is 65
octets long -- the shortest is a lone BIP340 signature, which its
count and push length make 66.
"""
text = str_from_string(sig, "base64 signature")
try:
return len(base64.b64decode(text.strip(), validate=True)) == _BMS_SIZE
except ValueError:
return False
def _redeem_script(pub_key: bytes) -> bytes:
"""Return the p2wpkh script a p2sh-p2wpkh address wraps."""
return b"\x00\x14" + hash160(pub_key)
def _assert_key_owns(script_type: str, payload: bytes, pub_key: bytes) -> None:
"""Refuse an address that is not this public key's, of any of the four.
The comparison is against what the key builds, one shape per type,
so an uncompressed key needs no case of its own: it hashes to a
p2pkh address and to no other, and the three segwit spellings it is
offered for fail here rather than after a signature was made.
"""
owned = {
"p2pkh": hash160(pub_key),
"p2wpkh": hash160(pub_key),
"p2sh": hash160(_redeem_script(pub_key)),
# check_validity=False: the length is the line's own test and
# the octets are what `PrvKeyData.pub` has just derived
"p2tr": (
output_pubkey(PubKeyData(pub_key, check_validity=False))[0]
if len(pub_key) == 33
else b""
),
}
if owned.get(script_type) != payload:
raise BTClibValueError("mismatch between private key and address")
[docs]
def sign(msg: Octets, prv_key: PrvKeyData, addr: String) -> Sig:
"""Return the BIP322 signature of a message for a single-key address.
The address is the argument and not something worked out from the
key, one key owning an address of each type: it is the challenge
being signed, and BIP322 has no default for it.
The key is the parsed form, as `ecc.bms.sign` takes it: the scalar
signs and the compression flag beside it says which SEC octets the
address was built from. A WIF is read once by
`b58.prv_key_data_from_wif`, and a scalar is stated with
`PrvKeyData(q, network, compressed)`.
p2pkh, p2wpkh, p2sh-p2wpkh and p2tr are what one private key
satisfies on its own, so they are what this signs; the taproot case
is the key path, with no script tree. The variant follows the BIP:
*simple* where the address is native segwit and the rest of
`to_sign` is therefore fixed, *full* where a script_sig has to be
carried.
Any other script -- multisig, a script path, a time lock -- is a
`Psbt` of `to_sign` signed and finalized by `btclib_wallet.psbt`, or a
`Descriptor.satisfy` over the signatures it needs, and then a `Sig`
of what comes out. This function is the case that needs neither.
"""
# asked here whatever `check_validity` the object was built with: a
# key the constructor was told not to check is one a caller may hold
assert_type(prv_key, PrvKeyData, "prv_key")
prv_key.assert_valid()
script_pub_key = ScriptPubKey.from_address(addr).script
script_type, payload = type_and_payload(script_pub_key)
q, compressed = prv_key.q, prv_key.compressed
pub_key = bytes_from_prv_key_int(q, compressed=compressed)
_assert_key_owns(script_type, payload, pub_key)
spend = to_spend(msg, script_pub_key)
# the redeem script goes into the script_sig before the hash is
# computed: `sig_hash.from_tx` dispatches on it, and the engine
# checks it against the p2sh hash in the output being spent
script_sig = serialize([_redeem_script(pub_key)]) if script_type == "p2sh" else b""
tx = to_sign(spend, script_sig)
if script_type == "p2tr":
# `sig_hash.taproot` and not `from_tx`, which reads the annex and
# the tapleaf commitment off the witness stack: there is no stack
# yet, this being what is about to build one. A key path spend
# has neither -- ext_flag 0, no annex, no message extension --
# so the three are what a signer states rather than reads.
# DEFAULT and not ALL: BIP341 gives the empty hash type the
# meaning of SIGHASH_ALL, and a signature spelling it out is the
# same commitment one byte longer
msg_hash = taproot_sig_hash(tx, 0, spend.vout, DEFAULT, 0, b"", b"")
tweaked = output_prvkey_from_merkle_root(q)
return Sig(Witness([ssa.sign_(msg_hash, tweaked).serialize()]))
msg_hash = from_tx(spend.vout, tx, 0, ALL)
# grind=False, against `sign_`'s own default: the BIP's reference
# implementation signs the plain RFC6979 signature, and its vectors are
# the whole interoperability claim of this function -- a ground one
# would be valid, a byte shorter, and equal to nobody's published bytes.
# Nothing is broadcast here either, so that byte buys nothing
signature = dsa.sign_(msg_hash, q, grind=False).serialize() + ALL.to_bytes(1, "big")
if script_type == "p2pkh":
tx.vin[0].script_sig = serialize([signature, pub_key])
return Sig(tx)
tx.vin[0].script_witness = Witness([signature, pub_key])
# native segwit is the whole of what the simple variant may carry:
# a wrapped one has a script_sig, which only the full variant has
# somewhere to put
return Sig(tx.vin[0].script_witness) if script_type == "p2wpkh" else Sig(tx)